【Java深入学习】synchronized源码优化-下

93 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

膨胀锁(轻量级锁变为重量级锁)

如果在尝试加轻量级锁的过程中cas操作无法成功,这时一种情况就是有其他线程对此对象已经加上的轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

Thread-1线程

static final Object obj = new Object();
public static void method1(){
    synchronized(obj){
        // 同步块A
    }
}

解释:

此时Thread-1加锁失败申请变为重量级锁,进入锁膨胀过程,即Object对象申请Monitor锁 让Object指向重量级锁地址,然后自己进入Monitor的EntryList 的阻塞列表 也就是BLOCKED状态

当Thread-0退出同步代码块解锁时,使用cas将Mark Word的值恢复给对象头失败,因为此时的Object对象中的Mark Word中记录的是Monitor地址,所以这时会进入重量级解锁流程,即按照Monitor地址 找到 Monitor对象 把Object对象的原数据由Thread-0线程存入Monitor里,设置Owner为null,唤醒EntryList中BLOCKED线程

注意:这里的锁膨胀过程 Thread-1进入阻塞列表前其实还有一段自旋过程,也就是后续的自旋锁

自旋锁

当我们的轻量级锁在锁膨胀变为重量级锁的过程中,Thread1线程不会直接就进入阻塞列表,它会进入一个循环 然后一直试探Thread0线程是否已经解锁,这样做的目的是 避免进入阻塞状态 减少不必要的上下文切换 增加执行速度。

注意:需要注意的是 自旋锁是指锁膨胀过程中,如果自旋锁过程中成功获取到了cpu 则依然会处于轻量级锁状态,如果自旋过程失败 则会升级为重量级锁

偏向锁(对轻量级锁的优化)

偏向锁是对轻量级锁的优化,既然是优化就说明轻量级锁还有缺点,我们来看下面这个例子

static final Object obj = new Object();
public static void method1(){
    synchronized(obj){
        // 同步块A
        method2();
    }
}
public static void method2(){
    synchronized(obj){
        // 同步块B
        method3();
    }
}
public static void method3(){
    synchronized(obj){
        // 同步块B
    }
}

这个代码我们如果用轻量级锁就会导致锁重入,也就是除了第一次的锁记录替换了对象的MarkWord,剩下的几次锁记录都是null 但是 依然会进行与MarkWord的替换操作 虽然替换失败 但是 替换本身就是一个cas,耗费时间,这就是我们的优化点。

如何优化

知道上述轻量级锁的缺点,偏向锁就对症下药,为了避免锁重入的cas操作 所以偏向锁的做法是:第一次调用线程时 用ThreadId替换MarkWord,之后如果再次调用这个线程 只需要检查这个ThreadId是否是自己的,这样就不用进行替换操作避免了不必要的cas

偏向锁的状态详解

如何判断是否为偏向锁,是通过MarkWord里的biased_lock,如果为0说明不是偏向锁,如果是1说明是偏向锁。

偏向锁的几种情况

1.偏向锁默认延迟一段时间生效,也就是一个线程最开始是normal状态 之后变为 Biased状态

2.偏向锁也可以通过jvm设置为立即生效

3.当竞争非常激烈是不建议使用偏向锁,可以通过jvm设置关闭偏向锁

4.当监视器对象 调用hashCode方法后 便禁用了偏向锁,原因需要从MarkWord的结果进行分析 因为normal状态与Biased状态的区别在于normal状态有hashCode站25位 但是hashCode是在调用hashCode方法之后才会出现的,hashCode一旦出现 则占用25位,这样Biased状态的thread:23 epoch:2就被占用 所以便不能再使用偏向锁