BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

聊聊并发(二)——Java SE1.6中的Synchronized

| 作者 方腾飞 关注 67 他的粉丝 发布于 2012年5月25日. 估计阅读时间: 11 分钟 | Google、Facebook、Pinterest、阿里、腾讯 等顶尖技术团队的上百个可供参考的架构实例!

 

1 引言

在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些情况下它并不那么重了,本文详细介绍了Java SE1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。

2 术语定义

术语

英文

说明

CAS

Compare and Swap

比较并设置。用于在硬件层面上提供原子性操作。在 Intel 处理器中,比较并交换通过指令cmpxchg实现。比较是否和给定的数值一致,如果一致则修改,不一致则不修改。

3 同步的基础

Java中的每一个对象都可以作为锁。

  • 对于同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前对象的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁存在哪里呢?锁里面会存储什么信息呢?

4 同步的原理

JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明,但是方法的同步同样可以使用这两个指令来实现。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处, JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个 monitor 与之关联,当且一个monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。

4.1 Java对象头

锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。

长度

内容

说明

32/64bit

Mark Word

存储对象的hashCode或锁信息等。

32/64bit

Class Metadata Address

存储到对象类型数据的指针

32/64bit

Array length

数组的长度(如果当前对象是数组)

Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下:

 

25 bit

4bit

1bit

是否是偏向锁

2bit

锁标志位

无锁状态

对象的hashCode

对象分代年龄

0

01

在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:

锁状态

25 bit

4bit

1bit

2bit

23bit

2bit

是否是偏向锁

锁标志位

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向互斥量(重量级锁)的指针

10

GC标记

11

偏向锁

线程ID

Epoch

对象分代年龄

1

01

在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下: 

锁状态

25bit

31bit

1bit

4bit

1bit

2bit

 

 

cms_free

分代年龄

偏向锁

锁标志位

无锁

unused

hashCode

 

 

0

01

偏向锁

ThreadID(54bit) Epoch(2bit)

 

 

1

01

4.2 锁的升级

Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率,下文会详细分析。

4.3 偏向锁

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。下图中的线程1演示了偏向锁初始化的流程,线程2演示了偏向锁撤销的流程。

关闭偏向锁:偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟-XX:BiasedLockingStartupDelay = 0。如果你确定自己应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁-XX:-UseBiasedLocking=false,那么默认会进入轻量级锁状态。

4.4 轻量级锁

轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁解锁:轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

5 锁的优缺点对比

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景。

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度。

如果始终得不到锁竞争的线程使用自旋会消耗CPU

追求响应时间。

同步块执行速度非常快。

重量级锁

线程竞争不使用自旋,不会消耗CPU

线程阻塞,响应时间缓慢。

追求吞吐量。

同步块执行速度较长。

6 参考源码

本文一些内容参考了HotSpot源码 。对象头源码markOop.hpp。偏向锁源码biasedLocking.cpp。以及其他源码ObjectMonitor.cpp和BasicLock.cpp。

7 参考资料

作者简介

方腾飞,阿里巴巴资深软件开发工程师,致力于高性能网络和并发编程,目前在公司从事询盘管理和长连接服务器OpenComet的开发工作。 博客地址:http://ifeve.com 微博地址:http://weibo.com/kirals


感谢张龙对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

希望能再次校对 by Dongbin Nie

这文章应该再严谨的校对一下,理一下结构和句子

Re: 希望能再次校对 by 龙 张

你好,这篇文章是我审校的,您觉得哪里值得商榷,愿闻其详。

关于轻量级锁的问题 by 罗 南钦

您好,我有一个问题想请教一下。那张轻量级锁膨胀图上,线程2在自旋获取锁的过程之后的那个失败是什么原因造成的?

Re: 关于轻量级锁的问题 by 方 腾飞

自旋了很多次却仍然得不到锁就会失败,毕竟自旋是耗费CPU的一种操作,所以JVM就会觉得当前不适合使用轻量级锁,而进行膨胀。

Re: 希望能再次校对 by yan lv

我感觉锁的撤销这里没有说清楚
首先对于持有偏向锁的线程,不能随时撤销偏向锁,只能在同步块执行完成之后,才能够撤销锁,否则线程1执行了部门同步块代码,这个时候锁撤销了,然后锁被线程2抢到了,会导致线程1释放了锁。这个是 有点问题的

Re: 希望能再次校对 by 方 腾飞

对的,从偏向锁撤销的图中你可以看到,偏向锁的撤销必须等到同步代码块执行完之后才可以。
文中提到偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),那么这个时间点有可能是未进入同步代码快,也有可能是退出的同步代码块时候。

Re: 希望能再次校对 by Gao Rex

请问一下 图中轻量级锁 和 重量级锁 不需要记录分代信息了? GC标记 空 这行代表什么意思? 还有 在线程2自旋后 失败 这个有个时间限制吗? 如果有可以配置吗? 线程2能够在线程1执行同步体的时候膨胀锁? 并且使线程1失去锁? 我不知道是不是理解对 应该是线程1完成所有操作后(包括cas mark word后) 然后线程2才能升级锁

Gao Rex的回复 by 方 腾飞

你提的问题非常好,以下是我的回复。

在线程2自旋后 失败 这个有个时间限制吗? 如果有可以配置吗?
应该是有的,应该不能配置,这个我再确认下。

线程2能够在线程1执行同步体的时候膨胀锁? 并且使线程1失去锁? 我不知道是不是理解对 应该是线程1完成所有操作后(包括cas mark word后) 然后线程2才能升级锁。
必须要执行完同步体,才能进行锁膨胀。但是并不需要等到线程1完成CAS Mark Word后,因为线程1必须在退出同步体时,通过Mark Word得知是否有其他线程在竞争锁。

偏向锁的撤销的问题 by liu bin

请问在"偏向锁的撤销"节之"它会首先暂停拥有偏向锁的线程,然后检查持..."中,这里的的“它”指的是谁,对应偏向锁一节附图中的哪个角色?谢谢

Re: 偏向锁的撤销的问题 by 钱 东波

偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
所以应该是其他尝试竞争偏向锁的线程,如图中的线程2

各种锁的使用问题 by 乔 建伟

了解这几种锁的用处在于根据不同的业务场景对JVM进行调优?淘宝在实际中对怎么运用这些知识?能稍加举例说明吗?

Re: Gao Rex的回复 by 方 腾飞

锁的升级不是需要同步块执行完成的。持有锁的线程在执行完同步块的时候检查下锁是否升级了,如果升级了就唤醒等待的线程重新竞争。

Re: 各种锁的使用问题 by 方 腾飞

根据不同的情况对JVM进行调优,比如关闭偏向锁。

偏向锁是如何转到轻量级锁的? by 郑 炜

偏向锁是如何转到轻量级锁的?"偏向锁的获得和撤销"那个图里没看出来是怎么转到轻量级锁的,这个能再讲的详细点么?

Re: 关于轻量级锁的问题 by 陈 良柱

这里确实不太清楚,通常理解的自旋是无尽的循环直到获得锁,如果能说清在什么条件下才进行锁升级就完美了。

Re: 偏向锁是如何转到轻量级锁的? by 方 腾飞

这个地方我也不大确定,文献上没有纪录,查看JVM源码也没找到。但是只有两种可能性,第一:循环一定的次数后升级,第二无尽循环直到获得锁才升级。你觉得那一种好?

Re: 偏向锁是如何转到轻量级锁的? by tang lv

你好,为什么要循环一定次数?这里看不太明白
偏向锁遇到其它线程抢占,马上升级为CAS,不就完了吗?

Re: 偏向锁是如何转到轻量级锁的? by 冯 光头

我的理解不一定对,
因为偏向锁 耗能肯定要比轻量级锁 要少 ,而且锁一旦升级是不能降下来的,所以JVM会尽量维持低能耗锁

Re: 偏向锁是如何转到轻量级锁的? by tang lv

我觉得,偏向锁是为了解决“大多数情况下锁不存在多线程竞争”,而不是解决所有,所以升级没必要那么繁琐

Re: 偏向锁是如何转到轻量级锁的? by 方 腾飞

解释得很好!

monitorexit插入的位置不对吧? by vince vince

应该是同步块结束的位置,而不是方法结束的位置吧?

一个对象变为加锁状态以后,那它无锁状态时保存的hashCode,对象分代年龄保存到哪儿去了? by vince vince

一个对象变为加锁状态以后,那它无锁状态时保存的hashCode,对象分代年龄保存到哪儿去了?
解锁以后要恢复的啊,肯定要找到地方放,难道放到当前线程的栈帧空间上了??

Re: 一个对象变为加锁状态以后,那它无锁状态时保存的hashCode,对象分代年龄保存到哪儿去了? by vince vince

。。。看错了,应该就是放到锁记录中了吧~~

请问线程1持有轻量级锁期间,线程2尝试获取轻量级锁,此时线程2会不会修改对象头的mark word? by vince vince

如果肯定会修改,那么线程1解锁的时候发现mark word被修改了,就会升级成重量级锁,但这样的话图中线程2修改mark word失败是因为“线程1获取了锁”就说不通了吧?
如果线程2要等线程1释放了锁才修改mark word,那就不会升级成重量级锁了啊~
或者是线程2如果自旋成功,则修改mark word,若失败则升级成重量级锁,但这样的话无论自旋成不成功都要修改mark word,就是第一种情况了啊~
感觉有点矛盾,没想通,请教楼主~

线程2获取偏向锁时,如果CAS失败,会不会自旋再试?还是直接升级成轻量级锁了? by vince vince

线程2获取偏向锁时,如果CAS失败,会不会自旋再试?还是直接升级成轻量级锁了?

Re: 偏向锁是如何转到轻量级锁的? by vince vince

个人理解第一种更好:
1.死循环一般是一种不太好的设计原则,尤其是在底层层面,毕竟CPU时间片是很宝贵的资源。
2.方案2理论上最优,因为线程2无尽循环获取到锁意味着偏向锁也不用升级了,永远不用同步,使用偏向锁就够了,但现实中存在线程1获取到锁后阻塞或死循环而不释放锁的情况(如线程1死循环收消息,只建连接而永远没有消息上来,但这并非程序的错误,这是代码逻辑无法避免的情况),这是线程2永远也获取不到锁,所以只能用方案1,超时就升级锁。
不知道理解得对不对?请指教!

4.3 疑问 by 尹 航

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID?
锁记录中为什么需要存储线程id,这个id不就是自己的线程id吗,保存没有意义,以后进入只是看对象头中的线程id是否指向自己就可以了。而且如果你改了对象头,对象的hashcode就丢失了。

偏向锁同步代码块未执行完成时,发生竞争的情况 by Gong Jacks

您好,通过文章,我发现这里存在几个疑惑: 假若目前对象Mark Word的Thread Id为线程A的线程Id,为偏向锁,并且偏向锁正在执行同步代码块中。此时线程B monitorenter,发现不是指向自己的Thread Id,但是为偏向锁,而此时持有偏向锁的线程A还未执行完成同步代码块,此时需要等待一个全局安全点(问题1: 等待全局安全点时,线程B应该如何?挂起?不会吧?),达到安全点后,开始撤销偏向锁: 如果持有偏向锁的线程依然活跃于同步代码块,则挂起偏向锁(问题2: 还是说那个安全点就是指连同线程A的同步代码块操作完成了?),升级为轻量锁,线程B建立锁记录,拷贝对象头Mark Word以及将对象头Mark Word指向当前锁记录,为了更简单我们就假设此时没有第三者,那么CAS操作成功,标记为轻量级锁,线程B开始执行,执行完成后,会检查对象头的Mark Word是否还是指向当前锁记录,如果有则释放锁并唤醒被挂起的线程,如果无,则释放锁,恢复到无锁状态,到最后了我们发现 线程A被挂起以后,就始终没有被唤醒的机会,是哪里出了遗漏呢?谢谢,希望能够指出我理解的错误。

Re: 请问线程1持有轻量级锁期间,线程2尝试获取轻量级锁,此时线程2会不会修改对象头的mark word? by xu kimi

个人理解,这里自旋成功和自旋失败修改的markword不是修改成同一种markword。以线程A持有当前轻量锁,线程B尝试获取锁为例:步骤①线程B以线程B栈上的lockrecord去修改markword,自旋修改成功则获取锁,如果失败则进入步骤②;步骤②线程B修改markword为重量锁(monitor对象),并把线程B本身加入monitor对象的队列,并把锁状态膨胀为10,这一步骤的操作都是原子操作。

Re: 偏向锁是如何转到轻量级锁的? by 卓 学腾

线程2获取偏向锁失败是不是直接暂停线程了吗?按你这个说法不是自旋吗?

Re: 偏向锁是如何转到轻量级锁的? by 卓 学腾

嗯,在周志明的《深入理解Java虚拟机》13.3.5小节里有说到,“如果说轻量级锁是在无竞争的情况下使用CAS操作消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了”,然后还提到,“当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定或轻量级锁定的状态”,不过这里也有几个问题没想明白,比如什么情况才会恢复到未锁定的状态?变成轻量级锁的状态,是由哪个线程是操作的?占有锁的线程,或者竞争锁的线程?

偏向锁在什么时刻会晋升为轻量级锁呢 by Rocky Peng

偏向锁在什么时刻会晋升为轻量级锁呢?

轻量级锁 关于锁膨胀的原因 by 马 海涛

你好,我看你在文章中写到轻量级锁解锁的时候,如果CAS失败,则表示当前锁存在竞争,锁就会膨胀成重量级锁,而你在图中画到自旋锁获取锁失败后(你评论里回复自旋一定时间后还获取不到所就)锁会膨胀为重量级锁,我想知道哪个会导致轻量级锁膨胀为重量级锁?还有就是图中的锁膨胀和最后的CAS替换Mark Word谁再前,谁在后?希望解答疑惑,感谢

完全照抄《深入理解java虚拟机》 by Li Ke

这不就是完全照抄《深入理解java虚拟机》吗?发这种东西没人管的?

关于偏向锁的竞争流程问题 by 武 伟

为什么线程2在检查了偏向锁的线程之后就直接cas修改mark word了?我的理解是,线程2在检查偏向锁的指向之后,会优先暂停线程1,检查锁状态,如果线程1已释放锁,才修改mark word,如果尚未释放锁,就升级为轻量锁,而不是直接修改mark word,这样有可能发生线程1还没执行完同步块,锁偏向就改变了。

关于轻量级锁释放的问题 by 武 伟

为什么锁释放的时候还会有竞争?不是只有持有锁的线程能释放锁吗?

轻量级锁释放的问题 by 武 伟

为什么锁释放会发生竞争?不是只有持有锁的线程才能释放锁吗?

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

37 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT