Java并发中偏向锁、轻量级锁和重量级锁的原理

3 篇文章 0 订阅

重量级锁

Java的对象头中的MarkWord

32位操作系统:

64位操作系统:

Monitor

  • 当使用synchronized获得锁时:
synchronized(obj){//重量级锁
    //临界区代码
}

obj对象的MarkWord中的指针(ptr_to_heavyweight_monitor)指向一个 OS提供的 Monitor对象

Monitor中的Owner记录谁是这个锁的主人。

  • 当另一个对象也要获取obj锁时:

发现obj所指向的Monitor的所有者为Thread1,此时Thread2加入 阻塞队列

  • 当Thread1执行完毕,释放锁后,虚拟机从obj对象指指向的Monitor的EntryList中唤醒一个线程,赋给它锁。

轻量级锁

使用场景:如果一个对象虽然有多个线程访问,但是多线程访问的时间是错开的(没有竞争),那么可以使用 轻量级锁 来优化。

轻量级锁的语法仍然是synchronized

static final Object obj = new Object();
public static void method1(){
    synchronized(obj){ //1 加锁
        method2();
    }//4
}

public static void method2(){
    synchronized(obj){ //2
        ...
    }//3
}

加锁:

  • 在栈帧中创建锁记录(Lock Record)对象(代码1):

左边是栈帧,右边是Java堆中的obj对象,每个线程的栈帧都会包含一个锁记录的结构,其中存储了锁定对象的Mark Word。

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

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

  • 如果cas失败,有两种情况:
  1. 如果有其它线程持有了obj的轻量级锁,这是表明有竞争,进入锁膨胀过程

  2. 如果是自己执行了synchronized锁重入 (代码中2的位置),那么再添加一条LockRecord作为重入的计数

这里也会进行cas替换操作,但是会失败。

解锁:

  • 在代码3的位置,退出synchronized代码块,如果有取值为null的所记录,表示有重入,这是重置锁记录,表示重入计数减一

  • 在代码4的位置,退出synchronized代码块,此时所记录的值不为null,这是使用cas将Mark Word的值恢复给obj对象头
  1. 成功,解锁成功

  2. 失败,说明轻量级锁进行了膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

static final Object obj = new Object();
public static void method1(){
    synchronized(obj){ //1 加锁
        method2();
    }//4
}

Thread-0执行method1方法,获得到轻量级锁,此时Thread-1执行method1方法,获取轻量级锁失败,进入锁膨胀流程:

  • ojb对象申请 Monitor 锁,让obj指向重量级锁地址,并且对象头所标志位改为10

  • 然后自己进入Monitor的EntryList 进入阻塞队列

  • 当Thread-0 退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入 重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程

自旋优化

当轻量级锁竞争时,会先进行自旋等待锁,如果自旋没有获得锁,才会膨胀为重量级锁

偏向锁

轻量级锁在每次锁重入时,仍然需要执行CAS操作,JAVA 6 中引入了偏向锁,来优化锁轻量级锁的锁重入。

static final Object obj = new Object();//1
public static void method1(){
    synchronized(obj){ //2
        method2();
    }//5
}
public static vpid method2(){
    synchronized(obj){ //3
        //临界区
    }//4
}
  • 1: 此时obj的对象头的Mark Word为:

  • 2:假设Thread-1是第一个获取该锁的线程,其线程ID为100,那么此时的Mark Word为:

  • 3:这里是一个锁重入,此时obj的对象头的Mark Word与2一样

  • 4:在重入锁释放锁的时候,偏向锁使用了一种 等到竞争出现才释放锁 的机制,这里也不会改变Mark Word ,同理在代码5处释放锁的时候也不会改变对象头

public static void test1(){
    Object lock = new Object();
    Thread t1 = new Thread(()->{
        log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
        synchronized (lock){
            log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
        }
        log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
    },"t1");
    t1.start();
    Thread t2 = new Thread(()->{
        try {
            t1.join();//等待t1线程终止
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
        synchronized (lock){
            log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
        }
        log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
    },"t2");
    t2.start();
}

运行结果:

从上面的运行结果可以看到:t2线程在获取锁的时候,因为对象头的线程id不是t2线程的id,所以偏向锁 升级为了轻量级锁;并且在释放锁之后,lock对象的Mark Word 被设置为无锁状态(001),那么下次线程获取改锁时会是一个 轻量级锁

偏向锁撤销

  • hashcode
Object lock = new Object();
log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
d.hashCode();
log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));

对象的哈希码是存放在Mark Word 中的,但是一旦存放了哈希码,Mark Word就没有多余的空间来存放线程Id了,此时lock对象的Mark Word 的锁状态就变为了无锁(001)。

  • 其他线程竞争锁

如前面demo的结果所示,t2线程在t1线程终止之后获得了一个轻量级锁。t2在申请锁的过程中,偏向锁被撤销,然后升级为轻量级锁赋给t2线程,t2线程释放锁后,lock对象的锁状态为无锁状态。

偏向锁的批量重偏向

在上面的偏向锁撤销中,我们发现当一个线程去竞争偏向其他线程的偏向锁时,偏向锁会撤销。偏向锁的撤销有这样一个机制:

如果有线程t竞争偏向其他的线程的次数累计达到19次,那么第19次之后竞争的偏向锁将会偏向线程t(线程t获得的不再是轻量级锁),这个机制叫做 批量重偏向

public static void test2() throws InterruptedException {
    List<Object> locks = new ArrayList<>();
    Thread t1 = new Thread(()->{
        for (int i = 0; i < 30 ; i++){
            Object lock = new Object();
            locks.add(lock);
            log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
            synchronized (lock){
                log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
            }
            log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
        }

    },"t1");
    t1.start();

    Thread t2 =  new Thread(()->{
        try {
            t1.join();//等待t1结束
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 30; i++){
            Object lock = locks.get(i);
            log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
            synchronized (lock){
                log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
            }
            log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
        }
    },"t2");
    t2.start();
}

运行结果:

可以看到第19个锁还是轻量级锁,第20个锁已经偏向了t2。

偏向锁的批量撤销

public static void test3() throws InterruptedException {
    List<Object> locks = new ArrayList<>();
    Thread t1 = new Thread(()->{
        for (int i = 0; i < 39 ; i++){
            Object lock = new Object();
            locks.add(lock);
            log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
            synchronized (lock){
                log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
            }
            log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
        }

    },"t1");
    t1.start();

    Thread t2 =  new Thread(()->{
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 39; i++){
            Object lock;
            lock = locks.get(i);
            log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
            synchronized (lock){
                log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
            }
            log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
        }
    },"t2");
    t2.start();

    Thread t3 =  new Thread(()->{
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 39; i++){
            Object lock;
            lock = locks.get(i);
            log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
            synchronized (lock){
                log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
            }
            log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable()));
        }
    },"t3");
    t3.start();
    t3.join();
    log.info("新建Lock对象:" + getMarkWord(ClassLayout.parseInstance(new Object()).toPrintable()));
    log.info("新建Object对象:" + getMarkWord(ClassLayout.parseInstance(new Object()).toPrintable()));
}

t2线程撤销了0-18个锁,19-38个锁进行了重偏向线程t2。

t3线程撤销了19-38个锁,在JVM中,Lock类型的锁一共被撤销了39次,我们看新建一个Lock对象,它的所类型如下:

从结果可以看到,新创建的Lock对象不再试偏向锁了。这就是偏向锁的批量撤销机制:

在JVM中,属于同一类型的偏向锁被撤销大于等于39次,以后新建该类型对应的锁将不再是偏向锁。

锁消除

public class LockRemoveTest{
    static int i = 0;
    public void method(){
        Object obj = new Object();
        synchronized (o){
            i++;
        }
    }
}

如果上面的代码被反复的执行超过一个阈值,JIT即时编译器就会优化这部分的字节码。其中,类似obj这样的 局部变量,它除了在本方法中可以被访问到,其他任何地方都无法访问,所以这里的同步块就没有任何意义,JIT就会优化这个同步块,取消同步操作。

我们可以使用-XX:-EliminateLocks来关闭锁消除优化。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值