synchronized和LOCK的实现原理---深入JVM锁机制--比较好

上传人:油条 文档编号:20333782 上传时间:2017-11-21 格式:DOC 页数:13 大小:160.50KB
返回 下载 相关 举报
synchronized和LOCK的实现原理---深入JVM锁机制--比较好_第1页
第1页 / 共13页
synchronized和LOCK的实现原理---深入JVM锁机制--比较好_第2页
第2页 / 共13页
synchronized和LOCK的实现原理---深入JVM锁机制--比较好_第3页
第3页 / 共13页
synchronized和LOCK的实现原理---深入JVM锁机制--比较好_第4页
第4页 / 共13页
synchronized和LOCK的实现原理---深入JVM锁机制--比较好_第5页
第5页 / 共13页
点击查看更多>>
资源描述

《synchronized和LOCK的实现原理---深入JVM锁机制--比较好》由会员分享,可在线阅读,更多相关《synchronized和LOCK的实现原理---深入JVM锁机制--比较好(13页珍藏版)》请在金锄头文库上搜索。

1、JVM 底层又是如何实现 synchronized 的?目前在 Java 中存在两种锁机制:synchronized 和 Lock,Lock 接口及其实现类是 JDK5增加的内容,其作者是大名鼎鼎的并发专家 Doug Lea。本文并不比较 synchronized 与 Lock孰优孰劣,只是介绍二者的实现原理。数据同步需要依赖锁,那锁的同步又依赖谁?synchronized 给出的答案是在软件层面依赖 JVM,而 Lock 给出的方案是在硬件层面依赖特殊的 CPU 指令,大家可能会进一步追问:JVM 底层又是如何实现 synchronized 的?本文所指说的 JVM 是指 Hotspot 的

2、6u23版本,下面首先介绍 synchronized 的实现:synrhronized 关键字简洁、清晰、语义明确,因此即使有了 Lock 接口,使用的还是非常广泛。其应用层的语义是可以把任何一个非 null 对象 作为 锁,当 synchronized 作用在方法上时,锁住的便是对象实例(this) ;当作用在静态方法时锁住的便是对象对应的 Class实例,因为 Class 数据存在于永久带,因此静态方法锁相当于该类的一个全局锁;当synchronized 作用于某一个对象实例时,锁住的便是对应的代码块。在 HotSpot JVM 实现中,锁有个专门的名字:对象监视器。 1. 线程状态及状态

3、转换当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:Contention List:所有请求锁的线程将被首先放置到该竞争队列Entry List:Contention List 中那些有资格成为候选人的线程被移到 Entry ListWait Set:那些调用 wait 方法被阻塞的线程被放置到 Wait SetOnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为 OnDeckOwner:获得锁的线程称为 Owner!Owner:释放锁的线程下图反映了个状态转换关系:新请求锁的线程将首先被加入到 ConetentionList 中,当某个拥有锁的线程(

4、Owner 状态)调用 unlock 之后,如果发现 EntryList 为空则从 ContentionList 中移动线程到EntryList,下面说明下 ContentionList 和 EntryList 的实现方式:1.1 ContentionList 虚拟队列ContentionList 并不是一个真正的 Queue,而只是一个虚拟队列,原因在于ContentionList 是由 Node 及其 next 指 针逻辑构成,并不存在一个 Queue 的数据结构。ContentionList 是一个后进先出(LIFO )的队列,每次新加入 Node 时都会在队头进行, 通过 CAS 改变

5、第一个节点的的指针为新增节点,同时设置新增节点的 next 指向后续节点,而取得操作则发生在队尾。显然,该结构其实是个 Lock- Free 的队列。因为只有 Owner 线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了CAS 的 ABA 问题。1.2 EntryListEntryList 与 ContentionList 逻辑上同属等待队列,ContentionList 会被线程并发访问,为了降低对 ContentionList 队尾的争用,而建立 EntryList。Owner 线程在 unlock 时会从ContentionList 中迁移线程到 EntryList,并会指定

6、 EntryList 中的某个线程(一般为 Head)为 Ready(OnDeck)线程。Owner 线程并不是把锁传递给 OnDeck 线程,只是把竞争锁的权利交给 OnDeck,OnDeck 线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在 Hotspot 中把 OnDeck 的选择行为称之为“ 竞争切换”。OnDeck 线程获得锁后即变为 owner 线程,无法获得锁则会依然留在 EntryList 中,考虑到公平性,在 EntryList 中的位置不 发生变化(依然在队头) 。如果 Owner 线程被 wait方法阻塞,则转移到 WaitSet 队列;如果

7、在某个时刻被 notify/notifyAll 唤醒, 则再次转移到 EntryList。2. 自旋锁那些处于 ContetionList、EntryList、WaitSet 中的线程均处于阻塞状态,阻塞操作由操作系统完成(在 Linxu 下通 过 pthread_mutex_lock 函数) 。线程被阻塞后便进入内核(Linux )调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能缓解上述问题的办法便是自旋,其原理是:当发生争用时,若 Owner 线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等一等(自旋) , 在 Owner 线程释放锁后,争用线程可能会立即得到

8、锁,从而避免了系统阻塞。但 Owner 运行的时间可能会超出了临界值,争用线程自旋一段时间后还是无法获得锁,这时争用线程则会停止自旋进入阻塞状态(后退) 。基本思路就是自旋,不成功再阻塞,尽量降低阻塞的可能性,这对那些执行时间很短的代码块来说有非 常重要的性能提高。自旋锁有个更贴切的名字:自旋 -指数后退锁,也即复合锁。很显然,自旋在多处理器上才有意义。还有个问题是,线程自旋时做些啥?其实啥都不做,可以执行几次 for 循环,可以执行几条空的汇编指令,目的是占着 CPU 不放,等待获取锁的机 会。所以说,自旋是把双刃剑,如果旋的时间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。显然,自旋

9、的周期选择显得非常重要,但这与操作系统、硬件体系、系统的负载等诸多场景相关,很难选择,如果选择不当,不但性能得不到提高,可能还会下降,因此大家普遍认为自旋锁不具有扩展性。自旋优化策略对自旋锁周期的选择上,HotSpot 认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。经过调查,目前只是通过汇编暂停了几个 CPU 周期,除了自旋周期选择,HotSpot 还进行许多其他的自旋优化策略,具体如下:如果平均负载小于 CPUs 则一直自旋如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞如果 CPU

10、处于节电模式则停止自旋自旋时间的最坏情况是 CPU 的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差)自旋时会适当放弃线程优先级之间的差异那 synchronized 实现何时使用了自旋锁?答案是在线程进入 ContentionList 时,也即第一步操作前。线程在进入等待队列时 首先进行自旋尝试获得锁,如果不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平。还有一个不公平的地方是自旋线程可能会抢占了 Ready 线程的锁。自旋锁由每个监视对象维护,每个监视对象一个。3. JVM1.6 偏向锁在 JVM1.6中引入了偏向锁,偏向锁主要解决无

11、竞争下的锁性能问题,首先我们看下无竞争下锁存在什么问题:现在几乎所有的锁都是可重入的,也即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的 HotSpot 设计,每次加锁/ 解锁都会涉及到一些 CAS 操作(比如对等待队列的CAS 操作) ,CAS 操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个 线程,之后的多次调用则可以避免 CAS 操作,说白了就是置个变量,如果发现为 true 则无需再走各种加锁/ 解锁流程。但还有很多概念需要解释、很多引入的问题需要解决:3.1 CAS 及 SMP 架构CAS 为什么会引入本地延迟?这要从 SMP(对

12、称多处理器)架构说起,下图大概表明了 SMP 的结构:其意思是所有的 CPU 会共享一条系统总线(BUS ) ,靠此总线连接主存。每个核都有自己的一级缓存,各核相对于 BUS 对称分布,因此这种结构称为“对称多处理器”。而 CAS 的全称为 Compare-And-Swap,是一条 CPU 的原子指令,其作用是让 CPU 比较后原子地更新某个位置的值,经过调查发现, 其实现方式是基于硬件平台的汇编指令,就是说 CAS 是靠硬件实现的,JVM 只是封装了汇编调用,那些 AtomicInteger 类便是使用了这些封装后的接口。Core1和 Core2可能会同时把主存中某个位置的值 Load 到自

13、己的 L1 Cache 中,当Core1在自己的 L1 Cache 中修改这个位置的值时,会通过总线,使 Core2中 L1 Cache 对应的值“失效”,而 Core2一旦发现自己 L1 Cache 中的值失效(称为 Cache 命中缺失)则会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信称为“Cache 一致性流量”,因为总 线被设计为固定的 “通信能力”,如果 Cache 一致性流量过大,总线将成为瓶颈。而当 Core1和 Core2中的值再次一致时,称为“Cache 一致 性 ”,从这个层面来说,锁设计的终极目标便是减少 Cache 一致性流量。而 CAS 恰好会导致 Ca

14、che 一致性流量,如果有很多线程都共享同一个对象,当某个Core CAS 成功时必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除 CAS,降低 Cache 一致性流量。Cache 一致性:上面提到 Cache 一致性,其实是有协议支持的,现在通用的协议是 MESI(最早由Intel 开始支持) ,具体参考:http:/en.wikipedia.org/wiki/MESI_protocol,以后会仔细讲解这部分。Cache 一致性流量的例外情况:其实也不是所有的 CAS 都会导致总线风暴,这跟 Cache 一致性协议有关,具体参考:http:/ Uniform Memory

15、Access Achitecture)架构:与 SMP 对应还有非对称多处理器架构,现在主要应用在一些高端处理器上,主要特点是没有总线,没有公用主存,每个 Core 有自己的内存,针对这种结构此处不做讨论。3.2 偏向解除偏向锁引入的一个重要问题是,在多争用的场景下,如果另外一个线程争用偏向对象,拥有者需要释放偏向锁,而释放的过程会带来一些性能开销,但总体说来偏向锁带来的好处还是大于 CAS 代价的。4. 总结关于锁,JVM 中还引入了一些其他技术比如锁膨胀等,这些与自旋锁、偏向锁相比影响不是很大,这里就不做介绍。通过上面的介绍可以看出,synchronized 的底层实现主要依靠 Lock-

16、Free 的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。JVM 中的另一种锁 Lock 的实现前文( 深入 JVM 锁机制-synchronized)分析了 JVM 中的 synchronized 实现,本文继续分析 JVM 中的另一种锁 Lock 的实现。与 synchronized 不同的是,Lock 完全用 Java 写成,在 java 这个层面是无关 JVM 实现的。在 java.util.concurrent.locks 包中有很多 Lock 的实现类,常用的有 ReentrantLock、 ReadWriteLock(实现类 ReentrantReadWriteLock) ,其实现都依赖 java.util.concurrent.AbstractQueuedSynchronizer 类,实现思路都大同小异,因此我们以 ReentrantLock 作为

展开阅读全文
相关资源
正为您匹配相似的精品文档
相关搜索

最新文档


当前位置:首页 > 行业资料 > 其它行业文档

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号