并发编程系列Synchronized实现原理

网友投稿 613 2022-05-30

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

1、了解synchronized字节码

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

public class SynchroinzedDemo { static int a; public static synchronized void add1(int b){ a += b; } public synchronized void add2(int b){ a += b; } public void add3(int b){ synchronized (this){ a += b; } } public static void main(String[] args) { } }

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

javap -verbose SynchroinzedDemo.class > log.txt

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

public static synchronized void add1(int); descriptor: (I)V flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field a:I 3: iload_0 4: iadd 5: putstatic #2 // Field a:I 8: return LineNumberTable: line 6: 0 line 7: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 b I public synchronized void add2(int); descriptor: (I)V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=2, args_size=2 0: getstatic #2 // Field a:I 3: iload_1 4: iadd 5: putstatic #2 // Field a:I 8: return LineNumberTable: line 10: 0 line 11: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 this Lcom/example/concurrent/sync/SynchroinzedDemo; 0 9 1 b I

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

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

综上:

方法上加synchronized,是在ACC_SYNCHRONIZED关键字

方法里加synchronized(obj),对应字节码是monitorenter、monitorexit

2、Monitor是什么?

前面的javap编译,我们知道了monitorenter和monitorexit,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的源码,路径:

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

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

// // The ObjectMonitor class is used to implement JavaMonitors which have // transformed from the lightweight structure of the thread stack to a // heavy weight lock due to contention // objectMonitor用于实现javaMonitor,javaMonitor是由于线程争用而从线程堆栈的轻量级结构转换为的重量级锁 // It is also used as RawMonitor by the JVMTI

// initialize the monitor, exception the semaphore, all other fields // are simple integers or pointers ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, // 等待中的线程数 _recursions = 0; //线程重入次数 _object = NULL; // 存储该monitor的对象 _owner = NULL; //拥有该monitor的线程 _WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet 指向第一个节点 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; //多线程竞争锁进入时的单向链表 FreeNext = NULL ; _EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; // 前一个拥有此监视器的线程ID }

主要方法:

bool try_enter (TRAPS) ; // 重量级锁入口方法 void enter(TRAPS); // monitor 释放 void exit(bool not_suspended, TRAPS); // 等待方法 void wait(jlong millis, bool interruptable, TRAPS); // 唤醒方法 void notify(TRAPS); void notifyAll(TRAPS);

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

3、锁的优化方法

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

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

public void doSomething(int size){ for(int i=0;i

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

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

@Override public synchronized StringBuffer append(Object obj) { toStringCache = null; super.append(String.valueOf(obj)); return this; }

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

public static String getStr(String str1, String str2) { StringBuffer sBuf = new StringBuffer(); sBuf.append(str1); sBuf.append(str2); return sBuf.toString(); }

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

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

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

-XX:-UseBiasedLocking

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

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

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

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

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

lock:2位的锁状态标记位,该标志位的值表示不同的锁状态,比如01表示正常无锁状态或者偏向锁状态,00表示轻量级锁状态,10表示重量级锁状态。

biased_lock:biased_lock为1时表示启动偏向锁,为0时表示对象没有启用偏向锁,biased_lock和lock配合表示的意义

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的指针。

轻量级锁的使用过程:

CAS锁修改mark word的lock标识为00,成功就获得锁,失败就是有竞争,自旋,自旋获取不到,转为重量级锁

抢到轻量级锁后将mark word保存到执行栈上,释放时CAS还原到对象头上,能还原成功,意味着没线程争用,还原不成功,则表示有线程竞争且阻塞等待了,唤醒等待线程,将mark word 复制给它。

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

-XX:+UseSpinning

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

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

4、锁的升级过程

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

偏向锁:当一个线程获取到锁后,这把锁就是偏向锁,偏向锁是在锁对象的对象头中记录一个线程id,然后该线程再次获取锁时,直接获取就可以

轻量级锁:如果有第二个线程来竞争锁,这时就会升级为轻量级锁,轻量级锁是不会阻塞线程的,其底层是通过自旋实现的。自旋是通过CAS获取一个预期的标识,如果没获取到,就会一直循环获取,获取到标识,也就标识获取到锁

重量级锁:如果轻量级锁一直自旋也获取不到锁,才会升级为重量级锁,重量锁是会阻塞线程的,也称之为重锁

5、参考资料

synchronized实现原理 | 小米信息部技术团队

《深入理解Java虚拟机》

https://www.cs.princeton.edu/picasso/mats/HotspotOverview.pdf

https://wiki.openjdk.java.net/display/HotSpot/Synchronization

Java JDK 任务调度

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:nginx--❤️图解及代码实现正向代理、反向代理及负载均衡(非常实用,建议收藏❤️)
下一篇:让我们由浅到深----------深刻了解python中进程创建、使用、管理等等。。。。。。
相关文章