Synchronized原理--轻量级锁

187 阅读4分钟

轻量级锁

轻量级锁是为了在线程近乎交替执行同步块时提高性能。 主要目的是在没有多线程竞争的前提下,通过 CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋再阻塞。

注意:多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。

轻量级锁流程:

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

在代码即将进入method1方法同步块的时候,如果此同步对象没有被锁定,虚拟机首先将当前线程的栈帧中建立一个名为Lock Record(锁记录)的空间,用于存储锁对象目前的Mark Word的拷贝地址。让锁对象中的Object Refrence用于指向锁对象地址。并尝试用CAS替换ObjectMark Word,将Mark Word的值存入锁记录中。如下图所示:

锁1.jpg

  1. 如果CAS替换成功,对象头中存储了锁记录地址和状态“00(轻量级锁)”,表示由该线程给对象加锁。 锁2.jpg

  2. 如果CAS失败,有两种情况:

  • 如果其它线程已经持有该Object的轻量级锁,此时表明有竞争关系,就会进入锁膨胀过程。
  • 如果自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数。案例如下:
public class Test{
    static final Object obj = new Object;
    public static void method1(){
        synchronized(obj){
            //同步代码块A
            method2();
        }
    }
    public static void method2(){
        synchronized(obj){
            //同步代码块B
        }
    }
}

锁3.jpg

  • 当退出method2方法中synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。如图: 锁2.jpg

  • 接下来当退出method1方法synchronized代码块(解锁时)锁记录的值不为null,这时CAS将Mark Word的值恢复给对象头。如果成功,则表示解锁成功。如图: 锁4.jpg 如果失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

轻量级锁的解锁

一句话总结轻量级锁的原理:将对象的Mark Word复制到当前线程的Lock Record中,并将对象的Mark Word更新为指向Lock Record的指针。

锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(存在竞争),这时候需要进行锁膨胀,将轻量级锁变为重量级锁。案例如下:

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

当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁。如图所示: 锁5.jpg

这时Thread-1加轻量级锁失败,进入锁膨胀流程

  • 即为Object对象申请Monitor锁,让Object指向地址“10”(重量级锁地址)。

  • 然后Thread-1自己进入MonitorEntryList BLOCKED12.jpg

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

自旋优化

什么是自旋优化?

比如有两个线程 A 和 B ,其中 A 获取到了锁,此时B不会挂起,而是时不时的去看看锁是否被释放了,即让 B 执行一个忙循环,这就是自旋锁。从代码的角度来看,自旋就相当于是循环。

自旋优化的优点?

解决线程的挂起和恢复都需要从用户态到内核态的转换所产生的开销。

自旋优化的缺点:

会额外耗费CPU资源。

自旋优化的使用场景:

锁被占用的时间很短,且线程数量不是太多的时候。

自旋重试成功

image.png

自旋重试失败

image.png

  1. 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  2. java6 引入了自适应自旋,JVM 会根据以往的运行信息来判断一个锁是否可以轻松的通过自旋获取到,如果是,会允许其他获取该线程的锁自旋更多的次数,反之 JVM 会直接挂起尝试获取这个锁的线程。
  3. Java7 之后不能控制是否开启自旋功能(总是开启)。