并发编程系列之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文件,使用如下命令:
代码语言:javascript复制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的源码,路径:
代码语言:javascript复制openjdkhotspotsrcsharevmruntimeobjectMonitor.hpp
penjdkhotspotsrcsharevmruntimeobjectMonitor.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
代码语言:javascript复制// 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
}
主要方法:
代码语言:javascript复制 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虚拟机团队对锁进行了重要的改进,为了优化其性能主要引入了偏向锁、轻量级锁、自旋锁、自适应自旋、锁消除、锁粗化等实现。
3.1、锁的粗化
为了保证多线程的有效并发,会要求每个线程持有锁的尽可能短,大部分情况,上面原则是正确的,但是在实际程序运行过程,可能会有一系列操作对一个对象反复加锁和解锁,或者加锁放在循环体中,这种情况会带来不必要的性能问题,所以jvm会对这种情况进行锁的粗化处理。锁粗化就是将锁的作用范围限制得尽可能小,只在共享数据的作用域中才进行同步加锁。
代码语言:javascript复制public void doSomething(int size){
for(int i=0;i<size;i ){
synchronized(lock){
// do something
}
}
// 锁粗化为,jvm可能会对程序优化,改变synchronized同步锁的作用范围
synchronized(lock){
for(int i=0;i<size;i ){
}
}
}
3.2、锁的消除
通过逃逸分析发现其实没有别的线程产生竞争的可能,别的线程没有临界量的引用,虚拟机会直接去掉这个锁
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锁
代码语言:javascript复制public static String getStr(String str1, String str2) {
StringBuffer sBuf = new StringBuffer();
sBuf.append(str1);
sBuf.append(str2);
return sBuf.toString();
}
3.3、偏向锁
jdk6引入了偏向锁来优化无线程争用时性能,偏向也即偏向获得它的线程,无锁化执行。
当一个线程获取到锁后,这把锁就是偏向锁。偏向锁是在对象头中记录一个线程ID,当这个线程再次去获取锁时,会校验是否这个线程,如果是直接获取锁就可以。
偏向锁可以提高带有同步但是无线程争用的程序性能,带有效益权衡性质的优化方法。也就是开启偏向锁并不一定都是有利的,如果程序总是存在多个线程竞争的情况,使用偏向锁反而影响性能,可以使用命令关闭偏向锁
代码语言:javascript复制-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_lock
和lock
配合表示的意义
biased_lock | lock | 状态 |
---|---|---|
0 | 01 | 无锁 |
1 | 01 | 偏向锁 |
- 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 复制给它。
3.5、自旋锁和自适应自旋
自旋是一种获取锁的机制,并不是锁的状态。如果业务场景比较简单,可以比较快完成,同时又有多个处理器的情况,则抢不到锁的线程是可以通过循环自旋的方式去获取锁。jdk1.4.2引入,默认关闭,jdk1.6改为默认开启,开关参数:
代码语言:javascript复制-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