侧边栏壁纸
  • 累计撰写 17 篇文章
  • 累计创建 10 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

[转载]synchronized锁升级过程和实现原理

骑摩托的程序员
2021-06-02 / 0 评论 / 2 点赞 / 1,345 阅读 / 0 字

一.synchronized升级过程

在最开始JDK1.0,1.2的时候,synchronized就是重量级锁,后来JDK对synchronized进行了一系列优化,这个优化就是有个升级过程。这个升级过程被markword清晰地进行了记录。

整个锁升级的过程大概分为:

new-->偏向锁-->轻量级锁(无锁,自旋锁,自适应自旋)-->重量级锁

我们结合下面这张图具体谈一下

20200704142013407.png

从刚刚new出来的时候,首先上的是偏向锁,出现争用,升级为轻量级锁,竞争状态变得特备激烈,变为重量级锁。这些信息全部记录在markword的8个字节中。这张图也说明了在不同的状态下,这64位到底是什么样的布局。在无锁态,头三位代表一个没有锁的状态。如果升级为偏向锁,也是头三位代表它为偏向锁。如果再升级为更重量级的锁,只用两位代表就行了。

刚new出来时,我们看下这64位都对应什么东西?

分代年龄占4位,其最大值即为15。这也就是我们所说的当分代年龄达到最大值时,就从年轻代变为老年代。

hashcode占31位,准确来说,对hashcode方法进行调用之后,它才会把这个值存进来,没有进行调用,这里面是没有值的。

我们看下调用前后内存布局变化:

20200704145056660.png

下面再谈下偏向锁,第一个拥有锁对象的线程,先不上那么重量级的锁,先给它贴个标签, 类比于在卫生间门上贴张写有名字的纸。偏向锁的意思就是它偏向于第一个刚刚进来的线程,因为在竞争不是那么激烈的情况下,其实一个线程拥有这把锁之后,很快就结束了,干完事之后就将这把锁释放了,所以没有必要申请重量级的锁,干脆贴个名字就完事了。所谓的往上贴个标签指的是在markword里面用54位记录了指向当前线程的指针。概括下,从new出来对象到上偏向锁,就是第一个线程来占位的时候,把自己的线程id往上一贴,这坑就属于它了。

当出现第二个线程的时候,就升级为轻量级锁。当B线程来抢坑的时候,发现门上已经贴有A线程的ID了,那A、B两个线程就开始抢坑了,抢坑的过程为:首先撤销偏向锁状态,即把门上标签撕了,每个线程都有自己的线程栈了,在各自的线程栈里生成自己的一个对象,这个对象叫做LockRecord(锁记录)。然后A、B线程开始抢,看谁能把各自LR指针给贴在坑位。也就说如果有任意一个线程抢到了这把轻量级锁的时候,这把轻量级锁里面就记录了指向线程栈中LockRecord的指针,占62位。这个抢的过程是采用自旋的方式,即CAS。用CAS操作把里面62位修改为指向我自己线程栈的LR指针。

竞争加剧后(JDK1.6之前指有线程超过10次自旋,-XX:PreBlockSpin,或者自旋线程数超过CPU核数的一半。JDK1.6之后,加入自适应自旋 Adapative Self Spinning,JVM自己控制。)升级为重量级锁。重量级锁存在于内核态,本质就是mutex数据结构,有数量限制的。用户态想申请重量级锁的时候,得像内核申请。内核给了我这把锁,我才拥有这把锁,这个时候在markword中占62位来记录指向重量级锁的指针。

那么为什么要从轻量级锁升级到重量级锁呢?轻量级锁本质就是执行一个循环,运行在用户态,效率高。但是这个循环是需要不断消耗CPU,如果我们竞争特别激烈,一个线程始终占着,10000个线程都在自旋,那CPU就很快100%了。升级为重量级锁后,维护有一个队列,而在这个队列里,如果没有轮到我执行的时候,是不消耗CPU的,处于wait状态。

下面多提2个概念:

锁消除 Lock eliminate

public void add(String str1, String str2){
    StringBuffer sb = new StringBuffer();
    sb.append(str1).append(str2);
}

我们都知道StringBuffer是线程安全的,因为它的关键方法是被synchronized修饰过的,但我们看上面这段代码,我们就会发现,sb这个引用只会在add方法中使用,不可能被其他线程引用(因为是局部变量,栈私有),因此sb是不可能共享的资源,JVM会自动消除StringBuffer对象内部的锁。因为你不断的append(str1).append(str2).append(str3)......有锁或无锁是没有区别。

锁粗化 Lock coarsening

public String test(String str){
    int i = 0;
    StringBuffer sb = new StringBuffer();
    while(i<100){
        sb.append(str);
        i++;
    }
    return sb.toString();
}

JVM会检测到这样一连串的操作都对同一个对象加锁(while循环内100次执行append,没有锁粗化就要进行100次加锁/解锁),此时JVM就会把加锁的范围放在while外部,使得一连串操作只需要加一次锁即可。 

二.synchonized的实现过程

1.在java代码层面,就是加了一个synchronized关键字

2.在字节码层面,就是加了monitorenter指令和monitorexit指令。进入锁状态和退出锁状态。

3.在JVM执行过程中,自动进行上述锁升级。

4.在汇编层面,就是用了lock comxchg这条指令。

20200704183911576.png

IF_MP 指如果是多个CPU。

至此便是锁的底层实现。 ———————————————— 版权声明:本文为CSDN博主「剑拔弩张的Logan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_38824137/article/details/107123782#

2

评论区