帮你深入了解synchronized关键字
学习这些锁之前先来了解一下MarkWord。
由此可见java的设计者们,真的是把mark-word设计到了极限。都是为了省内存啊。
内容都在图里。就不多赘述了。
什么是重量锁?
这个我觉得人人都应该清楚,其实java以前如果要解决线程安全问题。就要依赖于操作系统帮忙生成monitor监视器对象。你就认为是“锁”,由监视器对象来协调线程安全问题。
之所以叫 重量锁 就是因为如果使用这种方式,就要依赖于操作系统帮忙。会涉及到内核态和用户态的转换。(这块如果不理解可以百度哦)
java代码:
public class SyncDemo {
private Integer i = 0;
public void test(){
synchronized (this){
i++;
}
}
}
编译后的字节码:
public void test();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter // 这里向操作系统申请锁
4: aload_0
5: getfield #3 // Field i:Ljava/lang/Integer;
8: astore_2
9: aload_0
10: aload_0
11: getfield #3 // Field i:Ljava/lang/Integer;
14: invokevirtual #4 // Method java/lang/Integer.intValue:()I
17: iconst_1
18: iadd
19: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: dup_x1
23: putfield #3 // Field i:Ljava/lang/Integer;
26: astore_3
27: aload_2
28: pop
29: aload_1
30: monitorexit // 释放锁
31: goto 41
34: astore 4
36: aload_1
37: monitorexit // 异常失败也释放锁
38: aload 4
40: athrow
41: return
Exception table:
from to target type
4 31 34 any
34 38 34 any
其实重量锁,就是new 了一个monitor对象,
- 加锁时把对象(syn关键字锁的对象)的mark-word copy到monitor对象中,并将30bit保存为指向monitor的ref引用指针
- 解锁时复原回mark-word
什么是轻量锁?
上面说到,重量锁性能比较差,毕竟代码是要时时刻刻执行的。那么这群java设计者就想,我能不能在java用户态层面就把锁这个东西给控制住了。不需要使用那么重的监视器。
所以他们设计出了轻量锁,可以类比上面加锁和解锁过程。
只不过加锁解锁通过CAS 操作mark-word对象头。因为CAS操作是非常小并发代价。
CAS过程
- 加锁线程将对象(syn关键字锁的对象)的mark-word copy到本线程的栈帧中。
- 将原mark-word中的30bit 内容保存成指向栈帧的ref引用地址
- 解锁时同样,只要将栈帧中的copy-mark-word复原会对象头。
CAS 操作会有成功和失败。成功即表示加锁成功,失败即表示加锁失败。此时升级重量锁。
思考:
- 什么时候会失败?
- 如果一个线程已经加过轻量锁,但是被另一个线程申请成了重量锁。它CAS复原mark-word解锁时会怎么办?
什么是偏向锁?
因为轻量锁,毕竟还是要每次都CAS,虽然CAS性能非常高。但这群“疯子”想还能不能优化。不要每次都CAS。而且轻量锁还涉及mark-word的copy,势必也会影响到GC。
但程序中很多场景,虽然要保证线程安全,但是80%的时间都不会有资源竞争。
此时偏向锁闪亮登场:
即当第一个线程来加锁时,通过一次CAS操作,将自己的threadId保存进markword中。只要成功就代表加锁成功。以后该线程再来加锁时,只要对比一下自己的threadId和markword中的threadId是不是一样就可以了。偏向 偏向 意思就是这个锁已经偏向给第一个线程了。
是不是一直用偏向锁呢,什么时候会被打破?
肯定不是啊,那不然还要轻量和重量锁啥用。当第二个线程来加锁时(不管第一个线程是否解锁),就宣告锁对象的偏向模式结束。就被升级为轻量或者重量。
这几种状态怎么流转?
前面说了很多都是文字。下面我通过一张图来描述一下整个流程怎么转换:
附上《深入理解jvm虚拟机》中的流程转换: