Synchronized原理深入剖析我们肯定会遇到在同一个JVM中有多个线程同时操作同一个资源的情况。这时候,我们需要保证当一个操作的结果符合预期时,就需要使用同步的方式。官方解释:同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象的所有变量的读取或写入都通过同步方法完成。官方推荐的同步方式(JDK1.6之后):Synchronized是基于JVM实现的(这次是主角);当然,ReentrantLock是基于JDK实现的。先简单热身一下,给出一个通用的synchronized方法(锁就是这个类的实例对象)。publicclassSynchronizedCodeTest{publicvoidtestSynchronized()throwsInterruptedException{synchronized(this){System.out.println("输入同步代码块");线程.睡眠(100);System.out.println("离开同步代码块");}}publicstaticvoidmain(String[]args)throwsInterruptedException{newSynchronizedCodeTest().testSynchronized();}}Synchronized任何普通场景下的对象(带MarkWord结构,后面会详细介绍)都可以同步锁的对象,根据使用方式的不同,锁的对象和对应的粒度也不同.并发编程的三大特点简单回顾一下synchronized。当我们谈论锁时,我们会提到原子性、有序性和可见性。先简单介绍一下这些(我就不详细解释了,有需要的读者可以参考相关资料,或者有兴趣的我会在后面补充)。原子性原子性:一个操作或多个操作要么全部执行并且执行过程不会被任何因素打断,要么根本不执行。简单理解:如果将下单和支付这两个操作作为一个整体来考虑,只要有一个操作失败,就认为失败,否则就成功。Orderedorder:即程序执行的顺序按照代码执行的顺序执行。你可能或多或少听说过,Java为了提高性能允许重排序(编译器重排序和处理器重排序),因此程序执行可能会因此乱序。简单的理解就是:顺序保证了相同的代码在多线程和单线程执行时最终的结果是一样的,会按照代码的先后顺序执行。Visibility可见性:当多个线程访问同一个变量时,一个线程修改了变量的值,其他线程可以立即看到修改后的值。类的成员变量IntegerA=0;#线程1执行操作A=10;#同时线程2进行操作(B的值为0,不是10,这是可见性的问题)IntegerB=A;#常用的解决方案使用:volatile修改A或者使用synchronized修改代码块都可以解决这个问题。既然提到了synchronized,又扩展了2个特性。可重入同步监视器(锁定对象)有一个计数器。在获取锁的时候,会记录当前线程获取锁的次数。相应的代码块执行完后,计数器会一直为-1,直到计数器清零,锁才会上锁。释放。Uninterruptible不间断:一个线程获得锁后,另一个线程阻塞或等待。前一个不释放,后一个会一直阻塞或者等待,不能被打断。Synchronized是不可中断的,而ReentrantLock是可中断的(两者比较重要的区别之一)。在Synchronized字节码介绍完一些基本特性之后,我们正式开始分析synchronized的实现原理。#将上面的热身例子反编译成字节码javac-verboseSynchronizedCodeTest.javajavap-cSynchronizedCodeTest我们主要关注monitorenter和monitorexit这两条指令,分别对应当前线程获取lock&counter加一和释放lock&counter减一。多个线程获取一个对象的monitormonitor获取是互斥的。对象、对象监视器、同步队列和执行线程状态之间的关系任何访问对象的线程都必须首先获得对象监视器。如果获取失败,则线程进入同步状态,线程状态变为BLOCKED。释放监视器占用后,同步队列中的线程将有机会重新获取监视器。Java对象头(MarkWord)前面提到,所有的对象都可以作为同步锁的对象。同步时,是获取对象的监听,即对Java对象头中的MarkWord进行操作。以下是JVMMarkWord默认存储结构(无锁状态)对象的32位hashCode:25位对象标识符hashCode,采用懒加载技术。调用方法System.identityHashCode()计算并将结果写入对象头。当对象被锁定(偏向、轻量级、重量级)时,MarkWord的字节没有足够的空间来保存hashCode,所以会将值移到监视器上。**对象分代年龄**:4位Java对象年龄。这里记录了每次GC未被回收的累计年龄,默认达到15次进入老年代(-XX:MaxTenuringThreshold可以通过这个配置修改进入老年代阈值,最大值为15[年龄只有4位])。是否偏向锁:1位偏向锁标志。锁旗:2个锁旗,后面会显示4种旗。锁有4种状态,从低到高的级别分别是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这些状态会随着竞争形势逐渐升级。锁可以升级,不能降级,也就是说偏向锁升级为轻量级锁后,不能再降级为偏向锁。这种锁升级不降级策略的目的是为了提高获取和释放锁的效率。下面结合上图介绍这三种锁的实现原理和步骤。BiasedLockHotSpot作者通过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而总是由同一个线程多次获取。偏向锁的引入是为了让线程以更低的代价获取锁。偏向锁的获取当线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录中存储偏向锁线程ID。以后线程在进入和退出synchronized块时就不需要进行CAS操作了。加锁和解锁,只要测试对象头的MarkWord中是否存储了一个指向当前线程的偏向锁即可。如果测试成功,则线程获得了锁。如果测试失败,需要测试MarkWord中的偏向锁标志位是否设置为1(表示当前为偏向锁):如果没有设置,则使用CAS竞争锁;如果设置,尝试使用CAS设置对象头偏向锁指向当前线程。偏向锁的取消偏向锁使用了一种等待竞争发生后才释放锁的机制,所以当其他线程试图竞争偏向锁时,持有偏向锁的线程就会释放锁。偏向锁的获取和撤销流程Thread1--展示获取偏向锁的流程。线程2——展示了偏向锁取消的过程。轻量级锁介于偏向锁和重量级锁之间,竞争线程不会阻塞。轻量级加锁线程在执行同步块之前,JVM会先在当前线程的栈帧中创建一个存放锁记录的空间,并将对象头中的MarkWord复制到锁记录中,官方称之为Displaced标记单词。然后该线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针。如果成功,则当前线程获取锁。如果失败,说明有其他线程在竞争锁,当前线程尝试使用自旋来获取锁。轻量级解锁轻量级解锁时,DisplacedMarkWord将使用原子CAS操作替换回对象头。如果成功,则意味着没有竞争。如果失败,则说明当前锁处于竞争状态,该锁将扩展为重量级锁。下图是两个线程同时竞争锁,导致锁膨胀的流程图。轻量级锁及扩展流程图因为自旋消耗CPU,为了避免无用的自旋(比如获取锁的线程被阻塞),一旦锁升级为重量级锁,就不会再恢复为轻量级锁锁定状态。当锁处于这种状态时,其他线程试图获取锁时将被阻塞。当持有锁的线程释放锁时,这些线程就会被唤醒,被唤醒的线程会开始新一轮的抢锁竞争。重量级锁Synchronized是通过对象内部的监控锁(Monitor)来实现的。但是监听锁的本质是依赖底层操作系统的互斥锁来实现的。操作系统需要从用户态切换到核心态,才能实现线程间的切换。这个成本非常高,状态之间的转换需要比较长的时间,这也是Synchronized效率低下的原因。因此,这种依赖于操作系统MutexLock的锁被称为“重量级锁”。对比分析三种锁的原理后,选择哪种锁要看适用场景。最后,一个Synchronzied规避点(美团大佬分享):如果你的系统有明显的高低峰期,不建议使用Synchronized,可以考虑使用ReentrantLock。原因就是上面说的,Synchronized锁的扩容是不可逆的。这样一来,一旦过了高峰期,就永远是重量级锁,性能总会遇到瓶颈,无法提升。
