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

    技术2025-01-07  29

    重量级锁

    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失败,有两种情况:

    如果有其它线程持有了obj的轻量级锁,这是表明有竞争,进入锁膨胀过程

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

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

    解锁:

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

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

    成功,解锁成功

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

    锁膨胀

    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来关闭锁消除优化。

    Processed: 0.009, SQL: 9