绕不开的"锁"(三)

260 阅读4分钟

前言

上篇我们讲解了对象的存储结构,并学会了如何使用JOL来观察对象.下面我们继续利用该工具来观察Synchronized的锁升级过程.

锁升级

为什么会存在锁升级?根本原因就是为了提高Synchronized性能. 在早期的JDK实现中,Synchronized会借助监视器(也有叫管程的)向操作系统请求一个互斥锁,同时还要结合对象的wait setentry set来完成对线程间的同步.尤其向操作系统请求互斥锁时,涉及到用户态到内核态的切换,这个过程很耗时.后来发现,其实很多时候只有一个线程在竞争,这时没有必要向操作系统请求互斥锁.所以后来的JDK对Synchronized进行了一些列优化,也就是我们今天要说的锁升级.

所谓升级就是针对线程竞争的激烈程度动态改变加锁策略的过程.在升级过程中锁的状态会发生一系列变化,比如最开始没有竞争的时候直接使用偏向锁,等竞争开始时使用轻量级锁,竞争加剧时使用重量级锁.而这些锁状态就是存储在对象头的mark word中.

我们知道,Synchronized在工作时需要指定一个对象作为锁,如果没有显式指定,那么就使用当前类或其实例.对象刚被初始化时肯定是无锁状态.所谓无锁简单来说就是没有任何线程获得这把锁. 我们使用JOL来具体观察一下无锁状态下的mark word.

有如下类LockEscalation2,它包含一个成员变量i,一个常量LOCK以及一个同步方法add:

import org.openjdk.jol.info.ClassLayout;

public class LockEscalation2 {
  private int i=0;
  private final Object LOCK=new Object();
  public LockEscalation2(){
    System.out.println(ClassLayout.parseInstance(LOCK).toPrintable());
  }

  public void add(){
    synchronized (LOCK){
      while (i<10)
        System.out.println(i++);
    }
  }

  public static void main(String[] args) {
    LockEscalation2 escalation=new LockEscalation2();
    Thread thread = new Thread(()->escalation.add());
    thread.start();
  }
}

当LockEscalation2初始化时,由于没有任何线程获得锁对象LOCK,它此时它是无锁状态. 通过JOL我们能得到如下输出:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

由于锁状态只存储在mark word里,所以我们把代表mark word的前两行(8个字节)单独拿出来

 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)

由于mark word以小端序存储,为了方便观察,我们把它变成大端序

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

这些二进制位代表什么意思呢?看下面表格:

Mark Word (64 bits)State
unused:25 identity_hashcode:31 unused:1 age:4 biased_lock:1 lock:2Normal
  • unused 25 表示这25位暂时没用
  • identity_hashcode 31 表示这31位为对象的hashcode
  • unused 1 表示这一位也没用
  • age:4 表示GC分代年龄,因为只有4位,所以年龄最大值为15
  • bibiased_lock 1 表示是否可偏向.如果为0表示未偏向,为1表示已偏向
  • lock 2 表示lock的具体状态,它需要跟前面的bibiased_lock结合起来使用,具体关系如下: |biased_lock |lock |状态| |---|---|-| |0 |01 |无锁| |1 |01 |偏向锁| |0 |00 |轻量级锁| |0 |10 |重量级锁| |0 |11 |GC标记|
  • normal 表示无锁

上面mark word的最后三位001正好表示无锁.

总结

上面我们重点介绍了如何使用JOL来观察无锁状态下的mark word.随着线程间竞争加剧,mark word数据也会发生相应变化.下来我们会让线程竞争逐渐加剧,然后观察继续mark word的变化从而帮助大家彻底掌握锁升级过程.