12 | 多线程之锁优化(上):深入了解Synchronized同步锁的优化方法
12 | 多线程之锁优化(上):深入了解Synchronized同步锁的优化方法
讲述:李良
时长13:13大小12.10M
Synchronized 同步锁实现原理
锁升级优化
Java 对象头
1. 偏向锁
2. 轻量级锁
3. 自旋锁与重量级锁
动态编译实现锁消除 / 锁粗化
减小锁粒度
总结
思考题
赞 15
提建议
精选留言(122)
- bro.2019-06-18Synchronized锁升级步骤 1. 偏向锁:JDK6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能 , 2. 偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁 3. 当锁对象第一次被线程获取的时候,线程使用CAS操作把这个锁的线程ID记录再对象Mark Word之中,同时置偏向标志位1。以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。 4. 如果线程使用CAS操作时失败则表示该锁对象上存在竞争并且这个时候另外一个线程获得偏向锁的所有权。当到达全局安全点(safepoint,这个时间点上没有正在执行的字节码)时获得偏向锁的线程被挂起,膨胀为轻量级锁(涉及Monitor Record,Lock Record相关操作,这里不展开),同时被撤销偏向锁的线程继续往下执行同步代码。 5. 当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束 6. 线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录(Lock Record)的空间,并将对象头中的Mard Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果自旋失败则锁会膨胀成重量级锁。如果自旋成功则依然处于轻量级锁的状态 7. 轻量级锁的解锁过程也是通过CAS操作来进行的,如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中赋值的Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了,如果替换失败,就说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程 8. 轻量级锁提升程序同步性能的依据是:对于绝大部分的锁,在整个同步周期内都是不存在竞争的(区别于偏向锁)。这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢 简单概括为: 1. 检测Mark Word里面是不是当前线程ID,如果是,表示当前线程处于偏向锁 2. 如果不是,则使用CAS将当前线程ID替换到Mark Word,如果成功则表示当前线程获得偏向锁,设置偏向标志位1 3. 如果失败,则说明发生了竞争,撤销偏向锁,升级为轻量级锁 4. 当前线程使用CAS将对象头的mark Word锁标记位替换为锁记录指针,如果成功,当前线程获得锁 5. 如果失败,表示其他线程竞争锁,当前线程尝试通过自旋获取锁 for(;;) 6. 如果自旋成功则依然处于轻量级状态 7. 如果自旋失败,升级为重量级锁 - 索指针:在当前线程的栈帧中划出一块空间,作为该锁的锁记录,并且将锁对象的标记字段复制到改锁记录中!展开
作者回复: 赞
共 6 条评论96 - 学无止境00332019-06-19很少评论,但今天看的这个mark word对象和锁升级的图画的真是倍儿棒,一目了然,目前看过synchronized锁机制中最好理解的40
- 陆离2019-06-15非静态方法是对象锁,静态方法是类锁共 4 条评论27
- 不靠谱~2019-06-201.课后作业:实际对象锁和类对象锁的区别,锁对象不一样。 2. 1.8后CurrentHashmap已经不用segment策略了,想请教一下老师1.8后是怎样保证性能的呢? 3.对锁升级不太了解的同学可以看一下《Java并发编程的艺术》。里面有很详细的介绍,不过也是比较难理解,多看几遍。
作者回复: JDK1.8之后ConcurrentHashMap就放弃了分段锁策略,而是直接使用CAS+Synchronized方式保证性能,这里的锁是指锁table的首个Node节点。在添加数据的时候,如果Node数组没有值的情况,则会使用CAS添加数据,CAS成功则添加成功,失败则进入锁代码块执行插入链表或红黑树或转红黑树操作。
18 - nightmare2019-06-15加在普通方法锁对象是当前对象,其ObjectMonitor就是对象的,而静态方法上,锁对象就是字节码对象,静态方法是所有对象共享的,锁粒度比较大15
- 承香墨影2019-10-09老师,对我 waitSet 的理解也有歧义。 按您的在留言中的说法以及本文的内容,那等于进入 waitSet 会有两种情况,竞争 Monitor 失败,以及调用了 wait() 方法。 那何时会唤醒呢? 竞争 Monitor 失败的线程会在之前线程退出 Monitor 的时候再去竞争 Monitor,但是因外 wait() 方法也会进入 waitSet 的线程,就需要等待有线程退出的时候调用 notify() 方法,这一部分的细节和数据转换是怎么一回事?如何保证两种情况进入 waitSet 的线程,都拥有再次竞争 Monitor 的权利?展开
作者回复: 这里老师纠正下,当竞争Monitor失败后,是去到ContentionList队列,而运行中的线程调用了wait方法会进入到WaitSet队列,等调用notify方法,会去队列中唤醒相应的线程,进入到EntryList队列中。文中已更新。
共 2 条评论14 - 苏志辉2019-06-16entrylist和waitset那个地方不太理解,monitorenter失败后会进入entrylist吧,只有调用wait方法才会进入waitset吧,还请老师指点下
作者回复: 在获取到参与锁资源竞争的线程会进入entrylist,线程monitorenter失败后会进入到waitset,此时说明已经有线程获取到锁了,所以需要进入等待。调用wait方法也会进入到waitset。
共 7 条评论14 - 浩瀚有边2019-07-02老师,您好,synchronized锁只会升级,不会降级吧?如果系统只在某段时间高并发,升级到了重量级锁,然后系统变成低并发了,就一直是重量级锁了吗?请老师解惑,谢谢🙏
作者回复: 锁状态只能升级不能降级。
共 6 条评论13 - 天天向上2020-04-19老师在其他的回复中提到:synchronized锁只会升级,不会降级。如果系统只在某段时间高并发,升级到了重量级锁,然后系统变成低并发了,那还是重量锁,那岂不是很影响性能。
作者回复: 不应该叫锁降级,只是在垃圾回收阶段,即STW时,没有Java线程竞争锁的情况下,会将锁状态重置。
11 - 天天向上2020-04-19偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法。对此,有疑问,全局安全点指的是什么?什么情况下会出现暂停了该线程,该线程还在执行该方法?
作者回复: JVM在编译代码为字节码时,在字节码的边界都可以放一个安全点(safepoint),从线程角度看,safepoint可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这个位置暂停,暂停也就是我们说的发生stop-the-world(STW)。
9 - Wheat_Liu2020-05-31老师您好,想问一下,偏向锁的撤销为什么要在SafePoint暂停该线程呢,是因为要改变锁对象的头信息吗,那在线程运行时撤销偏向锁会出现什么问题呢
作者回复: 如果不暂停就不能正确判断线程是否正在持有偏向锁,暂停的目的是保证能正确判断线程持有偏向锁状态以及线程执行代码块的情况。
6 - 张三丰2020-04-08entryList有序吗?感觉这个结果多余,sync没有实现公平锁。
作者回复: 队列就是有序的,ContentionList会被线程并发访问,为了降低对ContentionList队尾的争用,而建立了EntryList。 sync的公平和非公平提现在进入ContentionList队列之前,有一个cas自旋获取锁操作,获取不到再进入队列。
6 - 钱2019-09-09课后思考及问题 首先,给老师的画图点个赞 这个太重要了,老师讲的相当棒,不过还是有些东西未消化,所以,特意多刷几遍。 再刷新时有如下疑问: 1:Mark Word 在 64 位 JVM 中的长度是 64bit,老师给出的图我计算了几次都不到64bit,是配图有问题嘛?另外,31bit21bit未使用和下面的54bit及2bit,那块没懂是什么意思? 2:在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁就会被撤销,发生 stop the word 后, 开启偏向锁无疑会带来更大的性能开销。 为啥发生 stop the word ? 开启偏向锁无疑会带来更大的性能开销,这个怎么理解,是因为发生了 stop the word,所以,才带来更大的性能开销还是别的什么带来的更大的性能开销? 3:JIT 编译器在动态编译同步块的时候,借助了一种被称为逃逸分析的技术,来判断同步块使用的锁对象是否只能够被一个线程访问,而没有被发布到其它线程。 确认是的话,那么 JIT 编译器在编译这个同步块的时候不会生成 synchronized 所表示的锁的申请与释放的机器码,即消除了锁的使用。 老师逃逸分析不太理解是啥意思?能否稍微再讲解一下? 4:锁粗化同理,就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。 发现第一次理解的锁粗化是错误的,锁粗化——粗化主要体现在将同一个锁实例相邻的同步代码快合并到了一起,使同步代码块的粒度变大了,不过减少了反复申请和释放同一个锁所带来的性能开销。展开4
- 张海鹏2019-09-06老师,您文中提到的锁消除一块没有十分理解,意思是若只有一个线程正在使用同步块,synchronized关键字就不被编译,就不加锁,当有新的线程也调用这个代码块的时候再加锁,是这样么?另外这个“借助了一种被称为逃逸分析的技术”可以扩展讲解一下么?
作者回复: 不是的,一旦锁消除了,就不会再使用该锁了。逃逸分析一般是对一个对象的作用域的分析,例如一个对象只能被一个线程访问到时,则会消除锁。
共 3 条评论4 - 晓杰2019-06-16感觉讲得有点晦涩啊,不知道其他人什么感觉
作者回复: 如果哪里不懂的,可以多提问,希望我能帮助到你。
共 2 条评论4 - 袁春栋2020-11-02老师您好,轻量级锁升级重量级锁的时候,是否会有这种情况,就是①偏向锁会在jvm启动时延迟4秒开启。②等待进行cas操作的线程数大于cpu核心线程数的二分之一将直接升级重量级锁4
- 耿嘉艺2020-06-15contentionList和EntryList有什么区别3
- Young2019-09-16请问老师,如果取消自旋,那轻量级锁和重量级锁还有什么区别吗
作者回复: 主要区别是获取锁的方式,如果没有自旋,轻量级锁是通过cas来获取锁的,cas失败则直接升级为重量级锁。
共 2 条评论3 - 钱2019-09-09课后思考及问题 1:Synchronized 是基于底层操作系统的 Mutex Lock 实现的,每次获取和释放锁操作都会带来用户态和内核态的切换,从而增加系统性能开销。 老师这段没完全明白能细致的描述一下嘛? 2:接下来当线程获取到对象的 Monitor 时,Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex。 Monitor对象和Mutex这两个具体又是什么?以及怎么实现的? 3:Synchronized同步锁的优化思路? 3-1:为啥要优化Synchronized同步锁? 因为它太重了,影响了系统的性能 3-2:Synchronized同步锁为啥这么重? 因为它底层的锁实现是依赖操作系统的Mutex锁实现的,依赖操作系统的底层锁实现,存在用户态和内核态的切换,因而会增加系统的开销。 3-3:用户态和内核态的切换为什么会增加系统的开销? 因为,线程从内核态切换到用户态时,需要保留线程当前的执行信息,待下一次切换回来后可以继续执行,所以,比较耗性能。正文老师没有讲到此点,评论区回复有这个信息,不过能给出更细致一些的描述就更好了,比如:都需要保存什么信息,保存这些信息花费的时间大概多少,花费的时间应该也有大有小吧。 3-4:理解了Synchronized同步锁为啥那么慢,那么耗性能,下面的锁优化其实就好理解了,他们所做的工作都在于减少做那些耗性能的事情。 3-5:偏向锁——自己获取自己加的锁,也需要用户态和内核态的切换,没必要,只有判断出自己在和自己竞争就不切换了,通过这种方式减少了用户态和内核态的切换。 3-6:轻量锁——使用CAS的方式尝试无锁操作是否OK,如果OK,也不需要用户态和内核态的切换,还是偏向锁,否则升级为轻量锁,轻量级锁采用是CAS的方式来操作,所以,也不会进行用户态和内核态的切换。不过它只适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争。 3-7:自旋锁——获取琐时,认为再等一会儿,其他线程就会释放锁了,所以,先等一下下。自旋就是空跑几圈CPU时钟周期不断的尝试获取锁,若获取到,则OK,否则就会升级为重量级锁。这是基于大多数情况下,线程持有锁的时间都不会太长,毕竟线程被挂起阻塞可能会得不偿失。 3-8:重量锁——这个本质和Synchronized就一样了,上面做了这么多尝试依据避免不了用户态和内核态的切换,那就只能切换了,慢一点总比错了强。 3-9:锁粗化——坦白讲上面的锁优化思路基本是投机取巧的策略,所以,具有一定的适用条件,如果取巧不成反而会更慢,于是又出现了这个锁优化,明确知道取巧的思路是不可行的那还是老老实实的进行用户态和内核态的切换吧! 再请教几个问题,操作系统层面是怎么实现锁的?另外,站在JVM的角度是不清楚锁什么时候被释放的嘛?如果能比较的清楚锁什么时候被释放,待其被释放的时候去获取或者多个线程竞争获取,这些性能是否更好一些。 WaitSet中的等待线程被唤醒重新进入EntryList,是有序进入还是无限的,我指的是在WaitSet中的次序?另外,所有竞争锁的线程都必须先进入EntryList嘛?进入后再获取监控器对象时就是有序的啦嘛?那公平锁和非公平锁,怎么提现和实现的?展开共 2 条评论3
- -W.LI-2019-06-18老师好!获取偏斜锁和轻量级锁的时候使用的CAS操作预期值传的是null(希望锁已释放),替换后值是当前线程什么?
作者回复: 老师没有看懂你问的具体问题,麻烦再描述一下你的问题。
3