Java锁详解[通俗易懂]

Java锁详解[通俗易懂]文章目录什么是锁锁的实现方式锁涉及的几个重要概念类锁和对象锁(重要)synchronized实现原理什么是锁计算机还是单线程的时代,下面代码中的count,始终只会被一个线程累加,调用addOne()10次,count的值一定就累加了10。publicclassTest{//计数器privateIntegercount=0;//累加…

大家好,又见面了,我是你们的朋友全栈君。

RocketMQ思维导图,不看会后悔哟
Mysql思维导图分享

上面思维导图可在gongzhonghao回复:扣扣号,获取联系方式后找我免费获得可编辑版本。 后面会继续分享其他思维导图,包括Redis、JVM、并发编程、RocketMQ、RabbtiMQ、Kafka、spring、Zookeeper、Dubbo等等

什么是锁

单线程的情况,下面代码中的count,始终只会被一个线程累加,调用addOne()10次,count的值一定就累加了10。

public class Test { 
   
    // 计数器
    private Integer count = 0;
    
    // 累加操作
    public void addOne() { 
   
        count += 1;
    }

    // 获取计算器的值
    public Integer getCount(){ 
   
        return this.count;
    }
}

而多线程情况下,有一个线程A调用addOne()10次的中间,就很可能会有另外一个线程B也在调用addOne()方法,这就会导致线程A调用getCount()的结果发现count的累加值会大于10。此时线程A就会觉得莫名其妙。所以对线程A来讲,count是线程不安全的。

要保证线程A调用10次,count的累加值也是10,则需要保证线程A在累加时,其他线程先排队等着。这就是多线程间的同步操作。

同步操作的实现,需要给对象关联一个互斥体,这个互斥体就可以叫做锁。不同的锁的实现方式不一样,这个后面会讲到。

锁的实现方式

Java中锁的实现方式有两种:synchronized关键字和并发包中的锁类。

synchronized 关键字是最基本也是最常见的一种同步方式。如:

public void synchronizedTest(){ 
   
  // 同步代码块 
  synchronized (this){ 
   
      // 一些业务操作 
      System.out.println(" synchronizedTest");     
   }
}

synchronized这个同步关键字以前性能不是太理想,在随着不停的优化后,它已经成了同步的首先。

并发包中的锁类基本上都是在JDK1.5以后才有的。如下面的可重入锁:

private ReentrantLock lock = new ReentrantLock();
 public void testLock() { 
   
        // 获取锁
        lock.lock();
        try { 
   
            Thread.sleep(3000);
        } catch (InterruptedException e) { 
   
            e.printStackTrace();
        }
        System.out.println("test ReentrantLock ");
        // 释放锁
        lock.unlock();
  }

synchronized也属于可重入锁。

锁涉及的几个重要概念

死锁

线程之间相互等着对方释放资源,而自己的资源又不释放给别人,这种情况就是死锁。所以,只要其中一线程释放了资源,死锁就会被解除。

重入锁

重入锁指的是,一个线程在拥有了当前资源的锁之后,可以再次拿到该锁而不被阻塞。在后面会讲到synchronized的重入锁原理。

自旋锁

自旋锁指的是,线程在没有获得锁时,不是被直接挂起,而是执行一个空循环(自旋)。默认是循环10次。

自旋锁的目的也就是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。

如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,空循环就会变成浪费系统资源的操作,反而降低了整体性能。所以,自旋锁是不适应锁占用时间长的并发情况的。

自适应自旋锁

自适应自旋锁是对自锁锁的一种优化。当一个线程自旋后成功获得了锁,那么下次自旋的次数就会增加。因为虚拟机认为,既然上次自旋期间成功拿到了锁,那么后面的自旋会有很大几率拿到锁。相反,如果对于某个锁,很少有自旋能够成功获得的,那么后面就会减少自旋次数,甚至省略掉自旋过程,以免浪费处理器资源。

这种锁是默认开启的。

锁消除

锁消除指的是,在编译期间利用“逃逸分析技术”分析出那些不存在竞争却加了锁的代码的锁失效。这样就减少了锁的请求与释放操作,因为锁的请求与释放都会消耗系统资源。

锁消除也是默认开启的。我们知道StringBuffer的append方法是加了锁的,但在下面的情况,它的锁就会失效:

public String test(){ 
   
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < 1000; i++) { 
   
        sb.append(i);
    }
    return  sb.toString();
}

逃逸分析技术,还会将确定不会发生逃逸的对象放在栈内存中而不是堆内存中,所以说,并不是所有的对象都存在堆内存中的。

锁偏向

偏向锁指的是,当第一个线程请求时,会判断锁的对象头里的ThreadId字段的值,如果为空,则让该线程持有偏向锁,并将ThreadId的值置为当前线程ID。当前线程再次进入时,如果线程ID与ThreadId的值相等,则该线程就不会再重复获取锁了。因为锁的请求与释放是要消耗系统资源的。

如果有其他线程也来请求该锁,则偏向锁就会撤销,然后升级为轻量级锁。如果锁的竞争十分激烈,则轻量级锁又会升级为重量级锁。

锁粗化

锁粗化指的是,在编译期间将相邻的同步代码块合并成一个大同步块。这样做可以减少反复申请和释放同一个锁对象导致的系统开销。锁粗化也是默认开启的。

粗化前伪代码:

synchronized(monitor){ 
   
    method1();
}
synchronized(monitor){ 
   
    method2();
}

粗化后伪代码:

synchronized(monitor){ 
   
    method1();
    method2();
}

锁粗化也提醒了我们平时写代码时,尽量不要在循环内使用锁:

// 粗化前
for(int i=0;i<10000;i++){ 
   
    // 这会导致频繁同步代码,无谓的消耗系统资源
    synchronized(monitor){ 
   
        doSomething...
    }
}
// 粗化后
synchronized(monitor){ 
   
    for(int i=0;i<10000;i++){ 
       
        doSomething...
    }
}

类锁和对象锁(重要)

如果你分不清类锁和对象锁,那你在代码中对于锁的使用和分析就很容易出问题。

对象锁占用的资源是对象级别,类锁占有的资源是类级别。

Class A { 
   
    // ==>对象锁:普通实例方法默认同步监视器就是this,
    // 即调用该方法的对象
    public synchronized methodA() { 
   
    }

    public  methodB() { 
        
        // ==>对象锁:this表示是对象锁
        synchronized(this){ 
     
        }
    }

    // ==>类锁:修饰静态方法
    public static synchronized methodC() { 
   
    }

    public methodD(){ 
   
        // ==>类锁:A.class说明是类锁
        synchronized(A.class){ 
   }
    }

    // 普通方法:任何情况下调用时,都不会发生竞争
    public common(){ 
   
    }
}

methodA,和methodB都是对当前对象加锁,即如果有两个线程同时访问同一个对象的methoA或methodB会发生竞争。如果两个线程访问的是不同对象的methodA和methodB则不会发生竞争。

methodC和methodD是对类加锁,即如果两个线程同时访问同一个对象的methodC和methodD会发生竞争,且两个线程同时访问不同对象的methodC和methodD是也会发生竞争。

如果一个线程访问methodA或methodB,另一个线程访问methodC或methodD,则这两个线程不会发生竞争。因为一个是类锁另一个是对象锁。类锁和对象锁是两个不一样的锁,控制着不同的区域,它们互不干扰。

5种类锁示例

Class A { 
   
    // 普通字符串属性
    private String val;
    // 静态属性
    private static Object staticObj;

    // ==>类锁情况1:synchronized修饰静态方法
    public static synchronized methodA() { 
   
    }

    public methodB(){ 
   
        // ==>类锁情况2:同步块里的对象是类
        synchronized(A.class){ 
   }
    }

     public methodC(){ 
   
         // ==>类锁情况3:同步块里的对象是字符串
        synchronized("A"){ 
   }
    }

    public methodD(){ 
   
        // ==>类锁情况4:同步块里的对象是静态属性
        synchronized(staticObj){ 
   }
    }

    public methodE(){ 
   
        // ==>类锁情况5:同步块里的对象是字符串属性
        synchronized(val){ 
   }
    }
}

补充:
两个线程分别访问一个类的静态synchronized和一个静态不加锁方法时,不阻塞。
两个线程分别访问一个类的静态synchronized和一个非静态synchronized方法时,不阻塞。

synchronized实现原理

开始讲一个后面要用到的概念,临界区:被同步保护的代码区域。也就是下面字节码中monitorenter和monitorexit指令之间的区域。

public void synchronizedTest() { 
   
    synchronized (this) { 
   
        System.out.println(" synchronizedTest");
    }
}

上述同步代码块对应的字节码:
在这里插入图片描述
在字节码中,位置3处有个monitorenter就是申请锁的指令,位置19处有个monitorexit就是释放锁的指令。

监视锁monitor 是每个对象都有的一个隐藏字段。申请锁成功之后,monitor就会成为当前线程的唯一持有者。线程第一次执行monitorenter指令后,monitor的值由0变为1。当该线程再次遇到monitorenter指令后,就会将monitor继续累加1。这也是synchronized实现重入锁的原理。

我们知道,JVM会有指令重排序的操作。Java会在位置3和位置4之间插入一个获取屏障,在位置18和19之间插入一个释放屏障,这两个屏障保证临界区内的任何操作都不会被指令重排序到临界区之外。加上锁的排他性,临界区内的操作便具有了原子性。

在monitorexit指令后还会插入一个StoreLoad屏障,该屏障保证了monitorenter和monitorexit指令是成对不混乱的,从而保证了synchronized既可并列又可嵌套。

总结

  • 同步操作的实现,需要给对象关联一个互斥体,这个互斥体就可以叫做锁

  • 锁的作用是,保证同一竞争资源在同一时刻只会有一个线程占有

  • Java中锁的实现方式有两种:synchronized关键字和并发包中的锁类

  • 锁的优化策略有:锁消除、锁偏向、自适应自旋锁、锁粗化

  • 尽量不要在循环内使用锁,以减少资源消耗

后面会接着介绍并发包里的几个锁,以及它们之间的区别

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/157054.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • cubieboard服务器系统,CubieBoard_搭建自己的系统.pdf

    cubieboard服务器系统,CubieBoard_搭建自己的系统.pdfCubieBoard_搭建自己的系统构建自己的CubieBoardDebianLinuxsoloforce汇编整理2013年10月10日1soloforce摘要本文在x86-64UbuntuLinux上为CubieBoard(包括A10单核和A20双核系统)构建一个基于ARMHF的DebianLinux,包括SPL、U-BOOT、内核(Kernel)、根系统…

  • C语言小游戏——贪吃蛇—-小白专用

    C语言小游戏——贪吃蛇—-小白专用C语言贪吃蛇小游戏个人小白,后期也做了一些改进,附原视频地址(点击即可)废话在后面直接上程序该程序在VS2019上可完美运行。#include<stdio.h>#include<stdlib.h>#include<Windows.h>#include<time.h>#include<conio.h>constexprautomaphigh=28,mapwide=84;structvirus{ intx;

  • dsp调试技巧_keil仿真调试

    dsp调试技巧_keil仿真调试DSP仿真调试步骤1.仿真调试步骤通过USB电缆连接仿真器至主机,操作方法如下:确保设备全部处于断电状态。安装仿真器JTAG电缆,并将JTAG接头连接至目标板。用USB电缆连接仿真器至主机。仿真器上电。目标板上电。2.注意事项目标板或者仿真器带电的情况下,不可插拔仿真器JTAG接口。USB电缆尽量不要使用台式机计算机的前置USB接口连接。正确的上电顺序为:a)设备全部断电b)仿真器JTAG街头连接目标板c)连接USB电缆或者网线d)仿真器上

  • 安全帽识别前端与后端功能分析[通俗易懂]

    安全帽识别前端与后端功能分析[通俗易懂]近年来,监管部门对建筑工地的要求越来越高了,为保障工地现场人员安全,智慧工地解决方案增加了更多的管理方式,其中安全帽识别已经成为智慧工地的重要管理手段。安全帽识别是通过视频分析来检测工作人员是否佩戴安全帽,属于人…

  • Ogre1.7.2 + CEGUI0.7.5配置[通俗易懂]

    Ogre1.7.2 + CEGUI0.7.5配置[通俗易懂]转载请说明出处!http://blog.csdn.net/zhanghua1816/article/details/6650509鉴于现在很多朋友开始学习研究Ogre或者CEGUI,不过很多朋友对如何配置这两个环境有很多问题,所以我把配置方法在此简单介绍一下,希望对大家有用,分享是一种快乐,大家共同进步嘛~~~。我这里的这种方法可能不是最简单的配置方法,但是我相信这种配置方法或许对

  • ES6 数组方法归纳整理

    ES6 数组方法归纳整理ES6操作数组方法1.判断是否为数组 letarr=[1,2,3] console.log(Array.isArray(arr))//true console.log(Array.isArray([]))//true2.创建数组newArray()创建数组如果使用Array构造函数传入一个数值型的值,那么数组的长度length属性会被设置为该值; letitems=newArray(2); console.log(items.length);//2

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号