引入JOL(Java Object Layout)来打印java对象头在内存中的字节码。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
对象创建时的状态有两种:
关闭了偏向锁或者在偏向延迟内创建的对象锁的状态为无锁(01)。
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
object对应的对象结构如下:
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
可以用虚拟机参数-XX:-UseBiasedLocking
关闭偏向锁,JDK8默认开启。
可以用虚拟机参数-XX:BiasedLockingStartupDelay
来调整偏向延迟的时间,默认4000ms,设置为0,表示不延迟。
偏向延迟时间后创建的对象锁的状态为匿名偏向锁(101)。
TimeUnit.SECONDS.sleep(5);
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
object对应的对象结构如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
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
为什么JVM默认开启偏向延迟?因为JVM虚拟机自己有一些默认启动的线程,里面有大量的同步代码,这些同步代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
测试代码如下:
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
System.out.println(ClassLayout.parseInstance(object).toPrintable());
object对应的对象结构如下:
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
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 48 e1 58 02 (01001000 11100001 01011000 00000010) (39379272)
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
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
object对象刚创建出来时由于还在偏向延迟内,所以状态为无锁(01),当使用synchronized进行同步时,无锁(01)就会升级为轻量级锁(00),当锁释放后,锁的状态就会变为无锁(01)。
测试代码如下
TimeUnit.SECONDS.sleep(5); // 也可以不休眠,设置偏向延迟时间为0,-XX:BiasedLockingStartupDelay=0
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
System.out.println(ClassLayout.parseInstance(object).toPrintable());
object对应的对象结构如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
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
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 98 47 02 (00000101 10011000 01000111 00000010) (38246405)
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
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 98 47 02 (00000101 10011000 01000111 00000010) (38246405)
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
当开启了偏向锁,到达偏向延迟时间后创建出来的对象锁的状态为匿名偏向锁(001),当使用synchronized进行同步时,锁会偏向于第一次持有锁的线程,锁状态对应的标志位不变,但是对象头中会存储持有锁线程的ID,当锁释放后,锁的状态不变,对象头的结构也不会变,便于下次曾经持有锁的再次来获取锁,只需要判断线程ID是否是自己,无需再次偏向,效率高。
测试代码:
TimeUnit.SECONDS.sleep(5); // 也可以不休眠,设置偏向延迟时间为0,-XX:BiasedLockingStartupDelay=0
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
object.hashCode();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
object对应的对象结构如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
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
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 8f 3d 44 (00000001 10001111 00111101 01000100) (1144884993)
4 4 (object header) 61 00 00 00 (01100001 00000000 00000000 00000000) (97)
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
当开启了偏向锁,到达偏向延迟时间后创建出来的对象锁的状态为匿名偏向锁(001),当计算对象的hashcode时,hashcode会覆盖偏向锁标志位的空间,所以锁的状态由匿名偏向锁(001)变为无锁(01)。
轻量级锁也称为自旋锁,无锁,自适应锁。
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
当一个对象的锁状态为偏向锁时,另外一个线程来竞争锁,不管持有锁的线程的同步块有没有执行完,这把锁都会进行撤销。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程(Stop the World),然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,先将对象恢复成无锁状态,再将无锁升级为轻量级锁(遍历偏向锁线程的栈帧,如果里面还有LockRecord记录,说明还持有锁,那么让持有偏向锁的线程持有轻量级锁,否则让另一个线程持有轻量级锁),最后唤醒暂停的线程。
也就是偏向锁升级为轻量级锁的过程中,会涉及到偏向锁的撤销,锁的撤销会消耗系统资源,所以,在锁竞争特别激烈的时候,用偏向锁未必效率高,还不如直接使用轻量级锁。这也是为什么jvm偏向锁的默认设置要延迟4s,因为在jvm启动过程中会开启多个线程,有大量的锁竞争的操作。
偏向锁升级为轻量级锁的代码演示:
TimeUnit.SECONDS.sleep(5); // 休眠5s,保证偏向锁开启
Object object = new Object();
Thread t = new Thread(() -> {
synchronized (object) {
}
});
t.start();
t.join(); // 等待t1执行完
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
System.out.println(ClassLayout.parseInstance(object).toPrintable());
运行结果如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 b8 8f 1d (00000101 10111000 10001111 00011101) (495958021)
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
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) e0 e7 90 02 (11100000 11100111 10010000 00000010) (43050976)
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
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
从上面的运行结果可以发现对象头的锁状态由偏向锁(101)变成了轻量级锁(00),最后锁释放后变成了无锁(01)。
测试代码如下:
TimeUnit.SECONDS.sleep(5); // 休眠5s,保证偏向锁开启
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable()); // 101
Thread t = new Thread(() -> {
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable()); // 101
try {
object.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ClassLayout.parseInstance(object).toPrintable()); // 10
}
});
t.start();
t.join();
TimeUnit.SECONDS.sleep(3); // 等待锁释放
System.out.println(ClassLayout.parseInstance(object).toPrintable()); // 01
运行结果如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
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
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 20 a2 1e (00000101 00100000 10100010 00011110) (513941509)
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
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 1a b2 06 1c (00011010 10110010 00000110 00011100) (470200858)
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
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
总结,当调用对象的wait()方法锁的状态将会直接从偏向锁(101)直接进入重量级锁(10)。
在jdk1.6以前,轻量级锁自旋超过10次或者自旋的线程数超过CPU核数的一半,就会升级为重量级锁,如果太多线程自旋,自旋就是个CAS获取锁的死循环,会导致CPU消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU),轻量级锁可以使用-XX:PreBlockSpin
来开启。JDK6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
升级重量级需要向操作系统申请资源,CPU从用户态切换会内核态用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间。
测试代码如下:
Object object = new Object();
Thread t1 = new Thread(() -> {
synchronized (object) {
System.out.println("thread name: " + Thread.currentThread().getName());
System.out.println(ClassLayout.parseInstance(object).toPrintable()); // 00
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1");
Thread t2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2); // 等待t1获取到锁,造成资源竞争
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object) {
System.out.println("thread name: " + Thread.currentThread().getName());
System.out.println(ClassLayout.parseInstance(object).toPrintable()); // 10
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
TimeUnit.SECONDS.sleep(10); // 休眠一会,锁的释放没那么快
System.out.println(ClassLayout.parseInstance(object).toPrintable()); // 01
运行结果如下:
thread name: t1
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 08 ef ea 1e (00001000 11101111 11101010 00011110) (518713096)
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
thread name: t2
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 5a b2 f7 1b (01011010 10110010 11110111 00011011) (469217882)
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
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
当t1持有轻量级锁的过程中,t2过来获取锁,造成资源的竞争,就会使得轻量级锁(00)膨胀为重量级锁(10),重量级锁释放后,锁的状态变回无锁。