帮你深入了解synchronized关键字
- 学习这些锁之前先来了解一下MarkWord。
- 什么是重量锁?
- 什么是轻量锁?
- 什么是偏向锁?
- 这几种状态怎么流转?
学习这些锁之前先来了解一下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_01: dup2: astore_13: monitorenter // 这里向操作系统申请锁4: aload_05: getfield #3 // Field i:Ljava/lang/Integer;8: astore_29: aload_010: aload_011: getfield #3 // Field i:Ljava/lang/Integer;14: invokevirtual #4 // Method java/lang/Integer.intValue:()I17: iconst_118: iadd19: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;22: dup_x123: putfield #3 // Field i:Ljava/lang/Integer;26: astore_327: aload_228: pop29: aload_130: monitorexit // 释放锁31: goto 4134: astore 436: aload_137: monitorexit // 异常失败也释放锁38: aload 440: athrow41: returnException table:from to target type4 31 34 any34 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虚拟机》中的流程转换: