并发编程系列之Synchronized实现原理

x33g5p2x  于2022-04-10 转载在 其他  
字(6.9k)|赞(0)|评价(0)|浏览(541)

并发编程系列之Synchronized实现原理

1、了解synchronized字节码

下面给出一个简单例子,synchronized关键字加在两个方法上,另外一个加在方法里

  1. public class SynchroinzedDemo {
  2. static int a;
  3. public static synchronized void add1(int b){
  4. a += b;
  5. }
  6. public synchronized void add2(int b){
  7. a += b;
  8. }
  9. public void add3(int b){
  10. synchronized (this){
  11. a += b;
  12. }
  13. }
  14. public static void main(String[] args) {
  15. }
  16. }

先用使用javac编译为class文件,或者在IDE直接运行就行,找到对应class文件,使用如下命令:

  1. javap -verbose SynchroinzedDemo.class > log.txt

找到log.txt文件,对比两个加了synchronized关键字的方法,都有ACC_SYNCHRONIZED这个标识

  1. public static synchronized void add1(int);
  2. descriptor: (I)V
  3. flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
  4. Code:
  5. stack=2, locals=1, args_size=1
  6. 0: getstatic #2 // Field a:I
  7. 3: iload_0
  8. 4: iadd
  9. 5: putstatic #2 // Field a:I
  10. 8: return
  11. LineNumberTable:
  12. line 6: 0
  13. line 7: 8
  14. LocalVariableTable:
  15. Start Length Slot Name Signature
  16. 0 9 0 b I
  17. public synchronized void add2(int);
  18. descriptor: (I)V
  19. flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  20. Code:
  21. stack=2, locals=2, args_size=2
  22. 0: getstatic #2 // Field a:I
  23. 3: iload_1
  24. 4: iadd
  25. 5: putstatic #2 // Field a:I
  26. 8: return
  27. LineNumberTable:
  28. line 10: 0
  29. line 11: 8
  30. LocalVariableTable:
  31. Start Length Slot Name Signature
  32. 0 9 0 this Lcom/example/concurrent/sync/SynchroinzedDemo;
  33. 0 9 1 b I

找到比较关键的monitorentermonitorexit关键字,monitorentermonitorexit关键字是什么?后面再介绍

  1. public void add3(int);
  2. descriptor: (I)V
  3. flags: ACC_PUBLIC
  4. Code:
  5. stack=2, locals=4, args_size=2
  6. 0: aload_0
  7. 1: dup
  8. 2: astore_2
  9. 3: monitorenter
  10. 4: getstatic #2 // Field a:I
  11. 7: iload_1
  12. 8: iadd
  13. 9: putstatic #2 // Field a:I
  14. 12: aload_2
  15. 13: monitorexit
  16. 14: goto 22
  17. 17: astore_3
  18. 18: aload_2
  19. 19: monitorexit
  20. 20: aload_3
  21. 21: athrow
  22. 22: return
  23. Exception table:
  24. from to target type
  25. 4 14 17 any
  26. 17 20 17 any
  27. LineNumberTable:
  28. line 14: 0
  29. line 15: 4
  30. line 16: 12
  31. line 17: 22
  32. LocalVariableTable:
  33. Start Length Slot Name Signature
  34. 0 23 0 this Lcom/example/concurrent/sync/SynchroinzedDemo;
  35. 0 23 1 b I
  36. StackMapTable: number_of_entries = 2
  37. frame_type = 255 /* full_frame */
  38. offset_delta = 17
  39. locals = [ class com/example/concurrent/sync/SynchroinzedDemo, int, class java/lang/Object ]
  40. stack = [ class java/lang/Throwable ]
  41. frame_type = 250 /* chop */
  42. offset_delta = 4

综上:

  1. 方法上加synchronized,是在ACC_SYNCHRONIZED关键字
  2. 方法里加synchronized(obj),对应字节码是monitorentermonitorexit

2、Monitor是什么?

前面的javap编译,我们知道了monitorentermonitorexit,synchronized重量级锁实现依赖于Monitor,所以需要介绍一下说明是Monitor,翻译过来是监视器?我们知道synchronized锁的作用和ReentrantLock的作用是一致的,所以synchronized实现同步的原理是否应该一样?实现上应该也要有互斥量,有等待队列,有重入计数。

前面的学习,我们知道synchronized锁的实现依赖于jvm,要实现锁就要有互斥量,jvm实现锁的方式是什么?
jvm也是程序,因为作为java程序和操作系统的中间件,所以可以直接使用操作系统提供的线程同步原语:mutex互斥量和semaphore信号量,当然也可以使用CAS锁

而jvm使用的Monitor又是什么?和jvm以及操作系统底层的线程同步原语又有什么关系?
Monitor,翻译过来可以说是官程,或者说是监视器
在使用操作系统底层的线程同步原语,需要程序员非常小心地控制mutex的down和up操作,否则很容易引起死锁等问题。为了更容易写出正确程序,所以在mutex和semaphore的基础上,提出了更高层次的同步原语monitor,当然monitor并不是操作系统提供的,而是由编译器,比如java的jvm自己去实现的,所以要使用monitor要确定编程语言是否支持,比如c语言就不支持,java语言支持,因为jvm已经实现了,所以说synchronized是jvm层面的锁

jvm如何实现monitor的?可以去github下载openJdk的源码,路径:

  1. openjdk\hotspot\src\share\vm\runtime\objectMonitor.hpp
  2. penjdk\hotspot\src\share\vm\runtime\objectMonitor.cpp

主要类是ObjectMonitor.cpp,看了源码实现:

  1. //
  2. // The ObjectMonitor class is used to implement JavaMonitors which have
  3. // transformed from the lightweight structure of the thread stack to a
  4. // heavy weight lock due to contention
  5. // objectMonitor用于实现javaMonitor,javaMonitor是由于线程争用而从线程堆栈的轻量级结构转换为的重量级锁
  6. // It is also used as RawMonitor by the JVMTI
  1. // initialize the monitor, exception the semaphore, all other fields
  2. // are simple integers or pointers
  3. ObjectMonitor() {
  4. _header = NULL;
  5. _count = 0;
  6. _waiters = 0, // 等待中的线程数
  7. _recursions = 0; //线程重入次数
  8. _object = NULL; // 存储该monitor的对象
  9. _owner = NULL; //拥有该monitor的线程
  10. _WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet 指向第一个节点
  11. _WaitSetLock = 0 ;
  12. _Responsible = NULL ;
  13. _succ = NULL ;
  14. _cxq = NULL ; //多线程竞争锁进入时的单向链表
  15. FreeNext = NULL ;
  16. _EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
  17. _SpinFreq = 0 ;
  18. _SpinClock = 0 ;
  19. OwnerIsThread = 0 ;
  20. _previous_owner_tid = 0; // 前一个拥有此监视器的线程ID
  21. }

主要方法:

  1. bool try_enter (TRAPS) ;
  2. // 重量级锁入口方法
  3. void enter(TRAPS);
  4. // monitor 释放
  5. void exit(bool not_suspended, TRAPS);
  6. // 等待方法
  7. void wait(jlong millis, bool interruptable, TRAPS);
  8. // 唤醒方法
  9. void notify(TRAPS);
  10. void notifyAll(TRAPS);

synchronized锁队列协作流程比较复杂,所以源码的本博客就不详细描述,读者可以参考博客synchronized实现原理 小米信息部技术团队,里面有对源码做一个比较清晰的分析

3、锁的优化方法

在jdk6中,java虚拟机团队对锁进行了重要的改进,为了优化其性能主要引入了偏向锁、轻量级锁、自旋锁、自适应自旋、锁消除、锁粗化等实现。

3.1、锁的粗化

为了保证多线程的有效并发,会要求每个线程持有锁的尽可能短,大部分情况,上面原则是正确的,但是在实际程序运行过程,可能会有一系列操作对一个对象反复加锁和解锁,或者加锁放在循环体中,这种情况会带来不必要的性能问题,所以jvm会对这种情况进行锁的粗化处理。锁粗化就是将锁的作用范围限制得尽可能小,只在共享数据的作用域中才进行同步加锁。

  1. public void doSomething(int size){
  2. for(int i=0;i<size;i++){
  3. synchronized(lock){
  4. // do something
  5. }
  6. }
  7. // 锁粗化为,jvm可能会对程序优化,改变synchronized同步锁的作用范围
  8. synchronized(lock){
  9. for(int i=0;i<size;i++){
  10. }
  11. }
  12. }
3.2、锁的消除

通过逃逸分析发现其实没有别的线程产生竞争的可能,别的线程没有临界量的引用,虚拟机会直接去掉这个锁

StringBuffer是线程安全的,因为这个类使用了很多synchronized锁,append方法也是

  1. @Override
  2. public synchronized StringBuffer append(Object obj) {
  3. toStringCache = null;
  4. super.append(String.valueOf(obj));
  5. return this;
  6. }

例子,所以给一个例子,使用append这个方法,不过只是单独在main里同步调用,因为sBuf变量是本地变量,append方法是同步操作,不会存在竞争(不会逃逸),所以这个程序运行过程jvm可能会进行锁消除,忽略Stringbuffer里的synchronized锁

  1. public static String getStr(String str1, String str2) {
  2. StringBuffer sBuf = new StringBuffer();
  3. sBuf.append(str1);
  4. sBuf.append(str2);
  5. return sBuf.toString();
  6. }
3.3、偏向锁

jdk6引入了偏向锁来优化无线程争用时性能,偏向也即偏向获得它的线程,无锁化执行。

当一个线程获取到锁后,这把锁就是偏向锁。偏向锁是在对象头中记录一个线程ID,当这个线程再次去获取锁时,会校验是否这个线程,如果是直接获取锁就可以。

偏向锁可以提高带有同步但是无线程争用的程序性能,带有效益权衡性质的优化方法。也就是开启偏向锁并不一定都是有利的,如果程序总是存在多个线程竞争的情况,使用偏向锁反而影响性能,可以使用命令关闭偏向锁

  1. -XX:-UseBiasedLocking
3.4、轻量级锁

jdk1.6引进了轻量级锁,轻量级锁是相对于重量级锁使用monitor而言的,前面学习,我们知道monitor是基于操作系统底层的线程同步原语。引进轻量级锁并不是为了替换重量级锁,而是为了在没有多线程竞争的前提下,使用轻量级锁,减少重量级锁使用操作系统底层互斥量带来的性能损耗。

所以轻量级锁适应条件是同一时间线程争用不严重的情况。“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验,如果满足,使用轻量级锁当然可以带来性能提升,如果存在竞争,则可能比重量级锁更慢。思考:轻量级锁使用什么来做互斥量?

答案是cas锁轻量级锁使用对象头中的mark work来做互斥判断

以上是java对象处于5种不同状态时,Mark Work中64个位的表现形式,每一行表示对象处于某种状态时的样子。其中各部分参数的意义:

  • lock:2位的锁状态标记位,该标志位的值表示不同的锁状态,比如01表示正常无锁状态或者偏向锁状态,00表示轻量级锁状态,10表示重量级锁状态。
  • biased_lock:biased_lock为1时表示启动偏向锁,为0时表示对象没有启用偏向锁,biased_locklock配合表示的意义
biased_locklock状态
001无锁
101偏向锁
  • age:表示对象的年龄。在jvm的gc中,对象在survivor区复制一次,年龄加1.jvm可以设置一个阈值,默认是15,对象年龄达到15后会进入老年代,可以通过命令-XX:MaxTenuringThreshold进行设置
  • identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。
  • thread:持有偏向锁的线程ID
  • epoch:偏向锁的时间戳。
  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。

轻量级锁的使用过程:

  1. CAS锁修改mark word的lock标识为00,成功就获得锁,失败就是有竞争,自旋,自旋获取不到,转为重量级锁
  2. 抢到轻量级锁后将mark word保存到执行栈上,释放时CAS还原到对象头上,能还原成功,意味着没线程争用,还原不成功,则表示有线程竞争且阻塞等待了,唤醒等待线程,将mark word 复制给它。
3.5、自旋锁和自适应自旋

自旋是一种获取锁的机制,并不是锁的状态。如果业务场景比较简单,可以比较快完成,同时又有多个处理器的情况,则抢不到锁的线程是可以通过循环自旋的方式去获取锁。jdk1.4.2引入,默认关闭,jdk1.6改为默认开启,开关参数:

  1. -XX:+UseSpinning

优缺点:如果锁占用时间很短,自旋等待的效果是不错的,反之会耗费处理器资源。同时自旋对处理器数量也有要求,必须要有多个处理器。

自适应自旋:jdk1.6引入了自适应自旋,意味着自旋时间不再固定,而是由前一次在同个锁上的自旋时间及锁的持有者状态来决定的。如果在同一个锁对象上,自旋等待获得锁,并且持有锁的线程正在运行,那么虚拟机就会认为这次自旋是成功的,进而它允许自旋等待更长的时间。如果很少成功获取锁,那在以后去抢锁时可能省略自旋的过程。有了自适应自旋,虚拟机对锁的状况预测会越来越准确。

4、锁的升级过程

锁的升级过程:无锁->偏向锁->轻量级锁->重量级锁

  • 偏向锁:当一个线程获取到锁后,这把锁就是偏向锁,偏向锁是在锁对象的对象头中记录一个线程id,然后该线程再次获取锁时,直接获取就可以
  • 轻量级锁:如果有第二个线程来竞争锁,这时就会升级为轻量级锁,轻量级锁是不会阻塞线程的,其底层是通过自旋实现的。自旋是通过CAS获取一个预期的标识,如果没获取到,就会一直循环获取,获取到标识,也就标识获取到锁
  • 重量级锁:如果轻量级锁一直自旋也获取不到锁,才会升级为重量级锁,重量锁是会阻塞线程的,也称之为重锁

5、参考资料

相关文章