Welcome everyone

锁升级过程

java 汪明鑫 489浏览 0评论

认识锁升级

Java锁是为了防止进程多线程并发访问临界资源造成数据安全问题的

申请锁需要发起系统调用,向操作系统内核申请锁,是一个比较重的操作

Jvm对锁做了多次优化,避免上来就直接怼成重量锁。

 

于是乎就有锁升级,带着学习的目的一探究竟,而不是浮在表面。

锁升级的过程就是无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁随着多线程竞争激烈锁升级的过程

 

synchronized (object)

是对象上锁,竞争锁的是线程

哪个线程抢到锁了,需要在对象上有一个标记,轻量级锁对象对象头上需要存储线程的线程栈中的锁记录(lock record)

重量级锁需要向操作系统申请了,重量级锁和对象的关联是这样的,

对象头存储的是指向一个monitor的结构,线程栈中又有指针指向对象,monitor中会指向哪个线程获得了锁

 

monitor结构

monitor是重量级锁,需要向操作系统内核申请锁,比较重,也比较耗资源

在这里提前贴个图做简单介绍

 

轻量级锁

两个方法同步块,利用同一个对象加锁

创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

 

让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存 入锁记录

 

如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁

 

如果有锁重入的话,还会生成一个lock record,  重入计数加一

 

当有退出锁同步代码块时,再会干掉一个lock record,重入计数减一

当都解锁成功时,会cas恢复对象头的mark word

轻量级锁也称自旋锁,线程等待时会占用CPU资源自旋等待

 

重量级锁

 

偏向锁

前面的轻量级锁和重量级锁都相对好理解,偏向锁相对晦涩些,一起来看看。。。

偏向锁 – markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向加锁的第一个线程 。

默认情况偏向锁有个4秒的时延, 因为JVM虚拟机自己有一些默认启动的线程,里面有很多同步代码,

同步代码启动时肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低,所以会延迟开启偏向锁。

如果没有等到偏向锁开启就上锁了,就直接升级到轻量级锁。

 

new出来的对象,如果等到偏向锁开启就是匿名对象,markword已经表明上了偏向锁,

但是线程id还是0,就是目前还没有偏向任何人,谁先来我偏向谁,偏向之后,如果再也竞争就会撤销偏向锁升级为轻量级锁。

如果有线程上偏向锁,markword线程id就是上偏向锁成功的线程id

 

 

package pers.wmx.springbootfreemarkerdemo.jol;

import java.util.concurrent.TimeUnit;

import org.openjdk.jol.info.ClassLayout;

/**
 * @author wangmingxin03
 * Created on 2021-11-19
 */
public class Main2 {

    public static void main(String[] args) throws InterruptedException {
        // 偏向锁是过几秒钟才启动,Jvm启动会有大量线程开启偏向锁延迟些启用
        // 所以可能大家试过好多次,都是无锁直接到轻量级锁或者重量级锁。。。

        // 尝试睡几秒,等一波偏向锁升级
        Thread.sleep(TimeUnit.SECONDS.toMillis(5));

        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());  // 101 (偏向锁)

        // 只要主线程上偏向锁
        synchronized(o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());  // 101 (偏向锁)
        }

        Thread t1 = new Thread(() -> {
            synchronized (o) {
                System.out.println("t1 lock " + Thread.currentThread().getName());
                System.out.println(ClassLayout.parseInstance(o).toPrintable());  // 000 (偏向锁撤销, 升级轻量级锁)
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            synchronized (o) {
                System.out.println("t2 lock " + Thread.currentThread().getName());
                System.out.println(ClassLayout.parseInstance(o).toPrintable());  // 010 (重量级锁)
            }
        });
        t2.start();

    }
}

 

如果没有前面sleep几秒的话

上来对象是无锁然后升级为轻量级锁,上面我们也解释了为什么有偏向锁启动时延

 

有JVM启动参数可以调整偏向锁启动时延,也可以开启合禁用偏向锁

//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking 
//启用偏向锁
-XX:+UseBiasedLocking 

 

感受一波

现在把时延调成0,默认已启动对象就上偏向锁,我们实验下

 

package pers.wmx.springbootfreemarkerdemo.jol;

import org.openjdk.jol.info.ClassLayout;

/**
 * @author wangmingxin03
 * Created on 2021-11-19
 */
public class Main3 {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());  // 101 (偏向锁)

        // 只要主线程上偏向锁
        synchronized(o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());  // 101 (偏向锁)
        }
    }
}

 

 

锁升级过程

 

 

 

总结来说就是 new -> 偏向锁 -> 轻量级锁 -> 重量级锁

对应对象头 markword 如下图

 

 

 

转载请注明:汪明鑫的个人博客 » 锁升级过程

喜欢 (5)

说点什么

您将是第一位评论人!

提醒
avatar
wpDiscuz