Synchronized介绍
是JVM内置的锁,基于Monitor机制实现,基于操作系统底层的互斥原语Mutex,
重量级锁,性能较低(在JDK1.5之后做了优化)
锁粗化(Lock Coarsening)
锁消除(Lock Elimination)
轻量级锁(Lightweight Locking)
偏向锁(Biased Locking)
自适应自旋(Adaptive Spinning)
同步方法是通过方法中的access_flag中设置ACC_SYNCHRONIZED标志来实现的
同步代码块是通过monitorenter
和monitorexit
来实现的,会导致在用户态和内核态之间切换。
1
UnsafeFactory . getUnsafe (). monitorEnter ( lock );
Synchronized使用
实例方法,锁住的事该类的实例对象
1
public synchronized void method (){}
静态方法,锁住的是类对象
1
public static synchronized void method (){}
同步代码块,锁住该类的实例对象
1
2
3
synchronized ( this ){
// ...
}
同步代码块,锁住该类的类对象
1
2
3
synchronized ( Test . class ) {
// ...
}
同步代码块,锁住的是配置的实例对象
如下面代码锁住的是String对象lock
1
2
3
4
String lock = "" ;
synchronized ( lock ) {
// ...
}
MESA模型
在管程的发展史用,有三中不同的模型:
现在最为广泛使用的事MESA模型。如下图所示:
wait()注意事项
1
2
3
while ( 条件不满足 ) {
wait ();
}
如果不小心使用notifyAll()
唤醒了当前在wait()
的线程,为了保证条件满足才能继续执行,条件不满足继续wait()
,需要使用while(条件)
来配合wait使用,防止虚假唤醒 。
wait()
方法还有一个超时参数,为了避免线程进入等待队列之后被永久阻塞了。
Synchronized中的管程模型
Java中的管程模型只有一个条件变量和等待队列。
Monitor机制
java.lang.Object 类定义了 wait()
,notify()
,notifyAll()
方法,这些方法的具体实现,依赖于 ObjectMonitor
实现。ObjectMonitor是使用c++实现的,位于hotspot源码的objectMonitor.hpp
文件中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor () {
_header = NULL ; // 对象头 markOop
_count = 0 ;
_waiters = 0 ,
_recursions = 0 ; // 锁的重入次数
_object = NULL ; // 存储锁对象
_owner = NULL ; // 标识拥有该monitor的线程(当前获取锁的线程)
_WaitSet = NULL ; // 等待线程(调用wait())组成的,是一个双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
FreeNext = NULL ;
_EntryList = NULL ; // 存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0 ;
}
获取锁时,是将当前线程插入到_cxq
队列的的头部。
释放锁时,默认策略(QMode=0
)是:
如果EntryList为空,则将_cxq
中的元素按原有顺序插入到_EntryList
,并唤醒第一个线程,也就是当_EntryList
为空时,是后来的线程先获取锁
_EntryList
不为空,直接从_EntryList
中唤醒线程。
其他策略:
QMode=2,cxq 优先于 EntryList。尝试直接从 cxq 中唤醒一个后继线程。 如果成功,后继线程需要将自己从 cxq 中解除链接。
QMode=3,积极地在第一时间将 cxq 中的元素排入 EntryList,这种策略确保最近运行的线程位于 EntryList 的头部。
QMode=4,如果cxq非空,把cxq队列放置到entrylist的头部(顺序跟cxq相反)
对象头
见JVM中类创建和内存分配机制
偏向锁
在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。
1
2
3
4
5
6
7
8
9
10
11
// StringBuffer内部同步
public synchronized int length () {
return count ;
}
// System.out.println 无意识的使用锁
public void println ( String x ) {
synchronized ( this ) {
print ( x ); newLine ();
}
}
JDK6开始默认开启偏向锁
新创建的对象的Mark Word
中的Thread Id为0,表示此时处于可偏向状态,但是当前未偏向任何线程,称为匿名偏向状态(anonymously biased)。
延迟偏向
状态跟踪
偏向锁撤销
调用HashCode()
调用wait()/notify()
轻量级锁
如果偏向锁失败,虚拟机不会立即将锁升级为重量级锁,而是尝试使用轻量级锁。轻量级锁适合线程交替执行的场景,如果同一时间存在多个线程竞争同一个锁的场景,则此时会导致轻量级锁膨胀为重量级锁。
状态跟踪
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class LockEscalationDemo {
public static void main ( String [] args ) throws InterruptedException {
log . debug ( ClassLayout . parseInstance ( new Object ()). toPrintable ());
//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread . sleep ( 4000 );
Object obj = new Object ();
// 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
obj . hashCode ();
//log.debug(ClassLayout.parseInstance(obj).toPrintable());
new Thread ( new Runnable () {
@Override
public void run () {
log . debug ( Thread . currentThread (). getName () + "开始执行。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
synchronized ( obj ){
log . debug ( Thread . currentThread (). getName () + "获取锁执行中。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
}
log . debug ( Thread . currentThread (). getName () + "释放锁。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
}
}, "thread1" ). start ();
Thread . sleep ( 5000 );
log . debug ( ClassLayout . parseInstance ( obj ). toPrintable ());
}
}
偏向锁升级轻量级锁
模拟两个线程轻微竞争的场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Slf4j
public class LockEscalationDemo {
public static void main ( String [] args ) throws InterruptedException {
log . debug ( ClassLayout . parseInstance ( new Object ()). toPrintable ());
//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread . sleep ( 4000 );
Object obj = new Object ();
// 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
//obj.hashCode();
//log.debug(ClassLayout.parseInstance(obj).toPrintable());
Thread thread1 = new Thread ( new Runnable () {
@Override
public void run () {
log . debug ( Thread . currentThread (). getName () + "开始执行。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
synchronized ( obj ) {
// 思考:偏向锁执行过程中,调用hashcode会发生什么?
//obj.hashCode();
log . debug ( Thread . currentThread (). getName () + "获取锁执行中。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
}
log . debug ( Thread . currentThread (). getName () + "释放锁。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
}
}, "thread1" );
thread1 . start ();
//控制线程竞争时机
Thread . sleep ( 1 );
Thread thread2 = new Thread ( new Runnable () {
@Override
public void run () {
log . debug ( Thread . currentThread (). getName () + "开始执行。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
synchronized ( obj ){
log . debug ( Thread . currentThread (). getName () + "获取锁执行中。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
}
log . debug ( Thread . currentThread (). getName () + "释放锁。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
}
}, "thread2" );
thread2 . start ();
Thread . sleep ( 5000 );
log . debug ( ClassLayout . parseInstance ( obj ). toPrintable ());
}
}
轻量级锁膨胀为重量级锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Slf4j
public class LockEscalationDemo {
public static void main ( String [] args ) throws InterruptedException {
log . debug ( ClassLayout . parseInstance ( new Object ()). toPrintable ());
//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread . sleep ( 4000 );
Object obj = new Object ();
new Thread ( new Runnable () {
@Override
public void run () {
log . debug ( Thread . currentThread (). getName () + "开始执行。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
synchronized ( obj ){
log . debug ( Thread . currentThread (). getName () + "获取锁执行中。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
}
log . debug ( Thread . currentThread (). getName () + "释放锁。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
}
}, "thread1" ). start ();
new Thread ( new Runnable () {
@Override
public void run () {
log . debug ( Thread . currentThread (). getName () + "开始执行。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
synchronized ( obj ){
log . debug ( Thread . currentThread (). getName () + "获取锁执行中。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
}
log . debug ( Thread . currentThread (). getName () + "释放锁。。。\n"
+ ClassLayout . parseInstance ( obj ). toPrintable ());
}
}, "thread2" ). start ();
Thread . sleep ( 5000 );
log . debug ( ClassLayout . parseInstance ( obj ). toPrintable ());
}
}
锁状态转换
Synchronized锁优化
偏向锁批量重偏向和批量撤销
从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。
原理
以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向**阈值(默认20)**时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。
每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。
当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。
应用场景
批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。
批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。
JVM的默认参数值
设置JVM参数-XX:+PrintFlagsFinal,在项目启动时即可输出JVM的默认参数值
1
int BiasedLockingBulkRebiasThreshold = 20 // 默认偏向锁批量重偏向阈值
我们可以通过-XX:BiasedLockingBulkRebiasThreshold
和 -XX:BiasedLockingBulkRevokeThreshold
来手动设置阈值
批量重偏向例子
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程,重偏向会重置对象 的 Thread ID
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Slf4j
public class BiasedLockingTest {
//延时产生可偏向对象
Thread . sleep ( 5000 );
// 创建一个list,来存放锁对象
List < Object > list = new ArrayList <> ();
// 线程1
new Thread (() -> {
for ( int i = 0 ; i < 50 ; i ++ ) {
// 新建锁对象
Object lock = new Object ();
synchronized ( lock ) {
list . add ( lock );
}
}
try {
//为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活
Thread . sleep ( 100000 );
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
}, "thead1" ). start ();
//睡眠3s钟保证线程thead1创建对象完成
Thread . sleep ( 3000 );
log . debug ( "打印thead1,list中第20个对象的对象头:" );
log . debug (( ClassLayout . parseInstance ( list . get ( 19 )). toPrintable ()));
// 线程2
new Thread (() -> {
for ( int i = 0 ; i < 40 ; i ++ ) {
Object obj = list . get ( i );
synchronized ( obj ) {
if ( i >= 15 && i <= 21 || i >= 38 ){
log . debug ( "thread2-第" + ( i + 1 ) + "次加锁执行中\t" +
ClassLayout . parseInstance ( obj ). toPrintable ());
}
}
if ( i == 17 || i == 19 ){
log . debug ( "thread2-第" + ( i + 1 ) + "次释放锁\t" +
ClassLayout . parseInstance ( obj ). toPrintable ());
}
}
try {
Thread . sleep ( 100000 );
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
}, "thead2" ). start ();
LockSupport . park ();
}
批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会认为不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
注意:时间-XX:BiasedLockingDecayTime=25000ms
范围内没有达到40次,撤销次数清为0,重新计时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@Slf4j
public class BiasedLockingTest {
public static void main ( String [] args ) throws InterruptedException {
//延时产生可偏向对象
Thread . sleep ( 5000 );
// 创建一个list,来存放锁对象
List < Object > list = new ArrayList <> ();
// 线程1
new Thread (() -> {
for ( int i = 0 ; i < 50 ; i ++ ) {
// 新建锁对象
Object lock = new Object ();
synchronized ( lock ) {
list . add ( lock );
}
}
try {
//为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活
Thread . sleep ( 100000 );
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
}, "thead1" ). start ();
//睡眠3s钟保证线程thead1创建对象完成
Thread . sleep ( 3000 );
log . debug ( "打印thead1,list中第20个对象的对象头:" );
log . debug (( ClassLayout . parseInstance ( list . get ( 19 )). toPrintable ()));
// 线程2
new Thread (() -> {
for ( int i = 0 ; i < 40 ; i ++ ) {
Object obj = list . get ( i );
synchronized ( obj ) {
if ( i >= 15 && i <= 21 || i >= 38 ){
log . debug ( "thread2-第" + ( i + 1 ) + "次加锁执行中\t" +
ClassLayout . parseInstance ( obj ). toPrintable ());
}
}
if ( i == 17 || i == 19 ){
log . debug ( "thread2-第" + ( i + 1 ) + "次释放锁\t" +
ClassLayout . parseInstance ( obj ). toPrintable ());
}
}
try {
Thread . sleep ( 100000 );
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
}, "thead2" ). start ();
Thread . sleep ( 3000 );
new Thread (() -> {
for ( int i = 0 ; i < 50 ; i ++ ) {
Object lock = list . get ( i );
if ( i >= 17 && i <= 21 || i >= 35 && i <= 41 ){
log . debug ( "thread3-第" + ( i + 1 ) + "次准备加锁\t" +
ClassLayout . parseInstance ( lock ). toPrintable ());
}
synchronized ( lock ){
if ( i >= 17 && i <= 21 || i >= 35 && i <= 41 ){
log . debug ( "thread3-第" + ( i + 1 ) + "次加锁执行中\t" +
ClassLayout . parseInstance ( lock ). toPrintable ());
}
}
}
}, "thread3" ). start ();
Thread . sleep ( 3000 );
log . debug ( "查看新创建的对象" );
log . debug (( ClassLayout . parseInstance ( new Object ()). toPrintable ()));
LockSupport . park ();
}
}
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。
Java 7 之后不能控制是否开启自旋功能
注意:自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)
锁粗化
假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。
1
2
3
4
5
6
7
8
StringBuffer buffer = new StringBuffer ();
/**
* 锁粗化
*/
public void append (){
buffer . append ( "aaa" ). append ( " bbb" ). append ( " ccc" );
}
锁消除
锁消除即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class LockEliminationTest {
/**
* 锁消除
* -XX:+EliminateLocks 开启锁消除(jdk8默认开启)
* -XX:-EliminateLocks 关闭锁消除
* @param str1
* @param str2
*/
public void append ( String str1 , String str2 ) {
StringBuffer stringBuffer = new StringBuffer ();
stringBuffer . append ( str1 ). append ( str2 );
}
public static void main ( String [] args ) throws InterruptedException {
LockEliminationTest demo = new LockEliminationTest ();
long start = System . currentTimeMillis ();
for ( int i = 0 ; i < 100000000 ; i ++ ) {
demo . append ( "aaa" , "bbb" );
}
long end = System . currentTimeMillis ();
System . out . println ( "执行时间:" + ( end - start ) + " ms" );
}
}
StringBuffer的append是个同步方法,但是append方法中的 StringBuffer 属于一个局部变量,不可能从该方法中逃逸出去,因此其实这过程是线程安全的,可以将锁消除。
测试结果: 关闭锁消除执行时间4688 ms 开启锁消除执行时间:2601 ms