本文参考视频博客来源:https://www.bilibili.com/video/BV1xT4y1A7kA?spm_id_from=333.999.0.0
1.Java中锁的理解
在并发状况下,如果存在多个线程对同一个资源进行抢夺的情况,就有可能会导致数据不一致的问题,在Java中就可以具体为多个线程对某个对象的争夺。为了解决这个问题,Java引入了锁机制,通过锁机制对资源的读取进行限制,当某个线程获取到锁时,其它的线程都要进行等待,本质上就是将所有的线程进行了串行化,解决了并发中出现的数据不一致问题。
那Java中具体是如何实现锁的呢?在具体介绍Java中的锁之前,我们先来了解一下Java虚拟机的内存模型,具体如下图所示:

从上图中,我们可以看出Java虚拟机的内存模型中主要包括有本地方法栈,程序计数器,虚拟机栈,堆,方法区,其中程序计数器,本地方法栈,虚拟机栈都是各个线程私有的,这些部分是不会出现线程竞争问题,而堆和方法区中保存的程序运行时的对象,类信息,静态变量,常量等信息区域都是有可能方式多线程竞争的区域。而我们在程序开发的过程中,锁也主要是应用在这些数据共享的区域。
那么Java中的锁具体是怎么实现的呢?Java中主要有两种锁的实现方式:一种是基于Object类的悲观锁,一种是基于CAS的乐观锁。对于悲观锁和乐观锁,除了从他们的实现方式上区别,还可以从他们的含义理解。即悲观锁认为接下来所有的操作都会导致线程不安全,所以在一开始就对其进行加锁,而乐观锁则是认为操作不一定会导致线程不安全,因此只在最后改写数据时才进行比较会不会出现线程不安全。而Java对悲观锁的实现,简单来说就是使用在每个对象头中存储一把锁,用来记录当前对象被那个线程所占有。
2.Java对象的组成
刚才我们说了Java中对于悲观锁的实现是在对象投中存储一把锁用于记录当前对象被那个线程占有,那么接下来让我们了解一下Java中对象具体是什么。
Java对象主要有三部分组成,分别为:①对象头;②实例数据;③填充字节。其中实例数据是初始化对象时设定的属性和状态等内容;而填充字节则是为了满足Java对象一定是8字节倍数大小而进行的填充,没有实际意义。
Java对象中与锁机制关系密切的主要是对象头,它由两部分,分别是Mark Word和Class Pointer其中ClassPointer主要是指向对象具体是哪个类的实例,而Mark Word则保留了对象许多运行时信息,如HashCode,锁状态标志,指向锁记录的指针,偏向线程ID,锁标志位。它不是一个结构化的形式,不同状况下它的结构也是不同的,具体我们用下图进行表示:

从上图我们可以看出Mark Word总共有32bit,其中最后两位是锁标志位,根据不同的锁标志位它也会有不同的结构,而锁的信息就存储在MarkWord中。而在Java中,我们开启对象锁的方式是使用Synchronized关键字,那Synchronized关键字对象头中的MarkWord又有什么关系呢?
3.Synchronized关键字
在Java中我们经常使用Synchronized来实现线程同步,而当我们对Synchronized关键字修饰的对象进行反编译后就会发现Synchronized底层其实就是通过monitorenter与monitorexit这两个字节码实现。
这里我们可以引入一个概念monitor,或者称它为管程。简单来说,我们可以把它理解为一个一次只能容纳一个人的房间,对象获取锁的得过程就可以类比为人进入房间,当一个线程获取锁就代表一个人进入房间,其它线程就只能在外等待,直到当前线程离开释放锁。而我们平时说的多线程加锁的过程其实也可以看做是对monitor对象的争夺过程。但是频繁对monitor的使用会带来性能问题,因为monitor的底层是使用操作系统mutex lock实现的,每次挂起或唤醒线程都会触发操作系统用户态和内核态的转化,这种切换是重量级十分耗时的,因此会导致整体系统的性能出现问题。
对于Synchronized导致的性能问题,Java采用了四种锁状态对其进行优化,即无锁,偏向所,轻量级锁,重量级锁,这四种锁状态的转化也就是我们常说的锁升级过程。
4.锁升级过程
结合我们之前所说的悲观锁实现和对象头中MarkWord的描述,我们很清晰的就可以对四种锁状态及其升级过程进行描述。
4.1无锁
所谓的无锁,其实就是指此时不会出现多线程竞争因此不需要加锁,或者可能出现多线程,但是我们通过使用CAS等无锁编程的方式去解决。此时在对象头的MarkWord的最后两位索标志位为01,倒数第三位为0表示不是偏向锁;
4.2偏向锁
偏向锁其实就是在程序运行中,我们给一个对象加了锁,但实际运行过程中只会有一个线程来使用它,那么我们希望这个对象能够认识这个线程,只要这个线程过来,我们就直接把对象交给它,实现不需要进行系统状态切换,在用户态就可以完成所有操作。这样看上去就好像这个对象偏爱这个线程一样,因此叫做偏向锁。
偏向锁的实现很简单,只需要将对象头中的MarkWord的最后两位为01,倒数第三位为1,这样线程就知道这个对象为偏向锁,接下来在MarkWord的前23位存放偏向的线程ID即可。
4.3轻量级锁
当对象发现开始有多个线程竞争时,那么偏向锁就会升级为轻量级锁。那么轻量级锁是如何实现线程与对象的绑定呢?首先MarkWord的锁标志位在此时变为00,而其前30位变成了指向虚拟机栈中锁记录的指针。
当一个线程想要获得某个对象的锁时,看见MarkWord的锁标志为00,就知道这是轻量级锁,此时该线程会在自己的虚拟机栈中开辟一块Lock Record空间,LockRecord中会存放对象头的MarkWord副本及Owner指针。此时,线程通过CAS 的操作去获取锁,如果获取成功,就会把对象头在的MarkWord复制到自己的Lock Record中,同时让自己的Owner指针执行当前对象;与此同时,该对象的MarkWord的前30位也会执行当前线程的LockRecord,这样就实现了对象与线程的绑定。
完成绑定后,对象就被这个线程锁定,那么此时如果有其他线程过来就会进入自旋状态,这里Java会有自适应性自旋策略,即如果这个线程之前获取过锁,那就适当延长它的自旋次数,默认是10次。但如果此时自旋的线程数量超过了当前CPU核数的一半,轻量级锁就会转换为重量级锁。
4.4重量级锁
重量级锁就像我们之前描述Synchronized关键字一样,它需要通过mutex lock锁定资源,对线程的控制最为严格但效率也会降低。
要注意,锁只能升级不能降级。
Java悲观锁与Synchronized详解及锁升级
本文介绍了Java中的锁机制,包括锁的理解、Java对象的组成、Synchronized关键字以及锁升级过程(无锁、偏向锁、轻量级锁、重量级锁)。Java通过对象头中的Mark Word实现锁,Synchronized通过monitorenter和monitorexit字节码实现线程同步,优化锁状态以提高性能。
170万+

被折叠的 条评论
为什么被折叠?



