Java synchronized原理

Posted 顧棟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java synchronized原理相关的知识,希望对你有一定的参考价值。

Java synchronized原理

文章目录


synchronized实现原理在JVM规范中有一个简单的介绍。JVM基于Monitor来实现方法级的同步和方法内部一段指令序列(代码块)的同步。

代码块的同步是由java语言中的synchronized表示的,JVM的指令集中有monitorenter和monitorexit两条指令,monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit指令是在编译后插入到同步代码块的结尾位置和异常位置。monitorenter与monitorexit是配对出现的。

每个对象都有一个monitor与之关联,当monitor被持有后,这个对象就处于锁定状态。

线程执行到monitorenter指令式,会尝试获取对象对应的monitor,即尝试获得对象的锁。

synchronized用的锁存在Java对象头中。先复习一下,对象的内存布局。

对象在内存中的布局

在Hotpot虚拟机中,对象在堆内存中布局有三个部分:对象头(Header)、实例数据(Instance Data)、对其填充(Padding)。这里主要用到对象头部分。

在对象头中包含两类信息,一类是用于存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等,这这部分被称为"Mark Word";还有一类是类型指针数据,即对象指向它的类型元数据的指针,JAVA虚拟机通过这个指针判断该对象是哪个类的实例。

JAVA对象头的长度

长度内容说明
32/64 bitMark Word存储对象的hashcode,锁信息等
32/64 bitClass metadata address存储到对象的类型数据的指针
32/32 bitarray length数组的长度(如果当前对象是数组)

不同状态的锁

从Java SE 1.6 为了优化锁的性能,引入了"偏向锁"和"轻量级锁"。

锁的状态:级别从低到高是无锁状态,偏向锁状态、轻量级锁状态、重量级锁状态,并且是针对Synchronized。这里的锁可以升级,不可以降级。

不同状态的锁对应的32位 Mark Word状态变化表

轻量级锁

为什么会有轻量级锁?

  1. 由于重量级锁中的阻塞(挂起线程/恢复线程), 需要转入内核态中完成,对性能影响很大。
  2. 锁住的代码块大多数情况都是在很短的时间执行完成。为了减少线程挂起和恢复的过度消耗,就使用了线程自旋重复尝试获取锁的方式,来替代线程的频繁挂起和恢复。

轻量锁的加锁

线程在执行到同步块之前,JVM会现在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间用于存储目前的Mark Word的拷贝(官方称为Displaced Mark Word);然后虚拟机使用CAS操作尝试把对象头中的Mark Word更新为指向Lock Record的指针,如果更新成功了,代表这个线程拥有了这个对象的锁,此时锁标志位也会变成“00”,代表这个对象处于轻量级锁状态,如果失败,表示其他线程在竞争该锁且已经被其他线程抢占了,当前线程会使用自旋来获取锁,如果在自旋失效期间还是未获得锁,说明锁需要膨胀为重量级状态。锁的标志位会变为“10”,此时Mark Word中存储的就是指向重量级锁的指针。当前线程以及其他需要这把锁的线程全部阻塞等待。

轻量锁的解锁

如果此时对象头中的Mark Word依然存储着当前线程Lock Record的指针,使用CAS操作将Displaced Mark Word和 Mark Word替换回来,如果替换成功,那就解锁了,如果替换失败,说明有其他线程在竞争这个锁,进入锁膨胀变成重量级锁,释放该锁后,通知等待该锁的线程进行新一轮的锁竞争。

偏向锁

为什么会有偏向锁?

  1. 无论是轻量级锁还是重量级锁,在进入与退出时都要通过CAS修改对象头中的Mark Word来进行加锁与释放锁。Hotpot的作者研究发现,大多数情况下不出现多线程的锁竞争,而且总是让同一线程多次获得,为了降低线程获得锁的代价才引入了偏向锁。

    偏向的意思是,偏向于第一个获取到这个对象锁的线程。如果在之后执行过程中,没有其他线程来获取这个对象锁,那么持有对象锁的线程将不进行同步,不发生切换,就节省资源。

  2. 轻量级锁中的自旋: 占用CPU时间,增加CPU的消耗(因此在多核处理器上优势更明显)。如果某锁始终是被长期占用,导致自旋如果没有把握好,白白浪费CPU资源。

    JDK5中引入默认自旋次数为10(用户可以通过-XX:PreBlockSpin进行修改), JDK6中更是引入了自适应自旋(简单来说如果自旋成功概率高,就会允许等待更长的时间(如100次自旋),如果失败率很高,那很有可能就不做自旋,直接升级为重量级锁,实际场景中,HotSpot认为最佳时间应该是一个线程上下文切换的时间,而是否自旋以及自旋次数更是与对CPUs的负载、CPUs是否处于节电模式等息息相关的)。

偏向锁的加锁

当锁对象第一次被线程获取的时候,JVM会将对象头中的锁标志位设为“01”,且是否偏向锁设置为“1”,表示进入了偏向锁状态。同时使用CAS操作,将把获取这个锁的线程的ID写到该对象的Mark Word中,如果CAS操作成功,说明获取偏向锁成功,以后这个线程执行这个锁相关的代码块时,JVM可以不进行任何同步操作(加锁,解锁,对Mark Word的更新操作),只需要简单的测试一下是否存储着指向当前线程的ID。

偏向锁的撤销

当其他线程尝试竞争偏向锁的时候,持有偏向锁的线程才会释放锁。当到了全局安全点(在这个时间点没有正在执行的字节码),会暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还存活,如果线程不存活,则将对象头的设置成无锁的状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,如果拥有偏向锁的线程已经退出同步块,则对象头中的Mark Word恢复到无锁状态,其他线程竞争该锁,使MarkWord重新偏向于其他线程;如果,原持有偏向锁的线程未退出同步块,则将偏向锁升级为轻量级锁,最后唤醒暂停线程继续执行。

重量级锁

当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。重量级锁是依赖Monitor来实现,而监视器锁(monitor)是依赖于底层的操作系统互斥Mutex Lock来实现的,而操作系统实现线程之间的阻塞、调度、唤醒等操作时需要从用户态切换到内核态,最后再由内核态切换到用户态,将CPU的控制权交由用户进程,用户态与内核态之间频繁的切换,严重影响锁的性能。

锁的不同状态关系流程图

synchronized的锁升级

所谓锁的升级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级。

当没有竞争出现时,默认会使用偏向锁(不关闭偏向锁的情况)。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。

如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏向锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

锁的对比

优点缺点适合场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级别差距如果线程间存在锁竞争会带来额外的锁撤销的消耗只有一个线程访问同步块场景
轻量级锁竞争的线程不会阻塞。提高了程序的响应速度如果始终得不到锁,会使用自旋空消耗CPU追求响应速度,同步块执行的时间特别快
重量级锁线程竞争不适用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行时间较长

锁优化

锁削除

虚拟机在即时编译器在运行时检测到某段需要同步的代码根本不可能存在数据竞争而实施的一种对锁进行消除优化策略。主要依据逃逸分析的数据检测到不可能存在竞争的锁,就自动将该锁消除。堆上的所有数据都不会逃逸出去被其他线程访问,可以把它们当作栈上数据对待,认为它们是线程私有的,无需同步加锁。

锁膨胀

为了让锁颗粒度更小,或者原生方法中带有锁,很有可能在一个频繁执行(如循环)中对同一对象加锁。由于在频繁的执行中,反复的加锁和解锁,这种频繁的锁竞争带来很大的性能损耗。如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,会把加锁的范围扩展到整个操作序列的外部,这样只需要加一次锁就可以了。这样的操作称为锁膨胀。

自旋的优化

JDK5中引入默认自旋次数为10(用户可以通过-XX:PreBlockSpin进行修改), JDK6中更是引入了自适应自旋(简单来说如果自旋成功概率高,就会允许等待更长的时间(如100次自旋),如果失败率很高,那很有可能就不做自旋,直接升级为重量级锁,实际场景中,HotSpot认为最佳时间应该是一个线程上下文切换的时间,而是否自旋以及自旋次数更是与对CPUs的负载、CPUs是否处于节电模式等息息相关的)。

是否开启偏向锁

项目中代码块中可能绝大情况下都是多线程访问。每次都是先偏向锁然后过渡到轻量锁,而偏向锁能用到的又很少。可以使用-XX:-UseBiasedLocking=false禁用偏向锁。


参考文章

Java Synchronised机制

《深入理解JAVA虚拟机》第三版 周志明版

《Java并发编程的艺术》 方腾飞 魏鹏 程晓明版


synchronized关键字

作用范围

  • 普通同步方法(成员变量和非静态方法) 锁住的是当前实例对象
  • 静态同步方法(静态方法) 锁住的是class实例
  • 同步方法代码块 锁住的是synchronized括号里中配置的对象

简单用法

待补

实现原理

内部区域

  • ContentionList:锁竞争队列
  • EntryList:竞争候选列表
  • OnDeck:竞争候选者
  • WaitSet:等待集合
  • Owner:取得锁的线程
  • !Owner:释放锁的线程

synchronized在收到新的请求时会先自旋取锁,若取锁失败则进入ContentionList。当owner线程释放锁之后会将ContentionList中一部分线程转移到EntryList,并且将EntryList中的某一个线程(最优先进入的)转为Ondeck。


以上是关于Java synchronized原理的主要内容,如果未能解决你的问题,请参考以下文章

synchronized关键字实现原理

synchronized关键字实现原理

Java并发编程:Synchronized及其实现原理

synchronized原理是啥?

Java并发编程:Synchronized及其实现原理

Java并发编程:Synchronized及其实现原理