JDK1.6对synchronized的锁优化

在JDK1.6之后,JVM团队对Java中的synchronized进行了优化,接下来让我们看看他们是如何进行优化的吧。

jdk1.6之前的synchronized

JVM是基于进入和退出Monitor对象来实现方法同步和代码块同步。

众所周知,synchronized是一个关键字,此关键字可以使作用在方法上或者是同步代码块中。如下:
JDK1.6对synchronized的锁优化_第1张图片
在这里插入图片描述
虽然两者都是使用synchronized进行了同步修饰,都能保证同步,也就是同一时刻只有一个线程在执行,但是他们还是有一点区别的。

方法上的synchronized

首先从表面上来说:

方法包括实例方法和类方法(也就是静态修饰的方法),如果synchronized作用在实例方法上,那么他持有的锁就是本对象,如果作用在静态方法上,那么他锁定的就是这个类。
注意:锁定实例和锁定类并不会发生锁竞争,不是说锁定类之后就相当于也锁定了对象,因为这根本就是两个不同的东西。
这两点的区别可以参考我另一篇博客中的8锁问题:https://blog.csdn.net/qq_42013590/article/details/103030098

另外从原理上来说:

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

代码块中的synchronized

和同步方法相比较,同步代码块锁定的对象是同步代码块括号中传过来的对象,传谁就是锁定谁。

从原理上:

代码块的同步是利用monitorenter和monitorexit这两个字节码指令(使用javap -c xxx.class反编译字节码查看汇编指令)。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

synchronized代码块的同步是利用monitorenter和monitorexit这两个字节码指令。
它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。
JVM可以从方法常量池中的方法表结构中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,最后再方法完成时释放monitor。

补充:Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。

jdk1.6之后的synchronized

在jdk1.6之后,对synchronized重量级锁进行了优化,不是直接获取到重量级锁,而是有一个锁膨胀的过程,这样能够在很大程度上减少了同步原语或者操作系统底层互斥量带来的性能性消耗。

偏向锁

偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的性能。

当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需测试Mark Word里线程ID是否为当前线程。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要判断偏向锁的标识。如果标识被设置为0(表示当前是无锁状态),则使用CAS竞争锁;如果标识设置成1(表示当前是偏向锁状态),则尝试使用CAS将对象头的偏向锁指向当前线程,触发偏向锁的撤销。偏向锁只有在竞争出现才会释放锁。当其他线程尝试竞争偏向锁时,程序到达全局安全点后(没有正在执行的代码),它会查看Java对象头中记录的线程是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程,撤销偏向锁,升级为轻量级锁,如果线程1不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

轻量级锁

轻量级锁的目的是在没有多线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

在代码进入同步块的时候,如果此对象没有被锁定(锁标志位为“01”状态),虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word)。然后虚拟机使用CAS操作尝试将对象的Mark Word更新为指向锁记录(Lock Record)的指针。
如果更新成功,那么这个线程就拥有了该对象的锁,并且对象的Mark Word标志位转变为“00”,即表示此对象处于轻量级锁定状态;如果更新失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块中执行,否则说明这个锁对象已经被其他线程占有了。该线程使用自旋的方式获取锁(减少互斥锁带来的性能浪费),当自选的次数到达一定的界限(后来有了自适应的自旋锁)就会膨胀为重量级锁。
如果有两条以上的线程竞争同一个锁,那轻量级锁不再有效,要膨胀为重量级锁,锁标志变为“10”,Mark Word中存储的就是指向重量级锁的指针,而后面等待的线程也要进入阻塞状态。

解锁过程:

轻量级锁解锁时,会使用CAS操作将Displaced Mark Word替换回到对象头,如果成功,表示没有竞争发生。如果失败,表示当前锁存在竞争,锁已经被升级为重量级锁,那么不仅要释放锁还要唤醒等待的线程。

如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁比传统重量级锁开销更大。

所有的细节都可以参照《深入理解Java虚拟机》这本书,我觉得已经讲得很详细了,有必要去阅读。

参考:《深入理解Java虚拟机》

你可能感兴趣的:(JUC,并发编程,多线程,并发,jvm)