目录
先来引入锁的概念:
偏向锁:当前只有一个锁,无线程竞争的情况下,尽量减少不必要的轻量锁的执行路径。
偏向锁就是在运行过程中,对象的锁偏向某个线程,即在开启偏向锁的情况下,某个线程获得锁,当该线程下次想要获得锁时,不需要再获取锁(忽略synchronized关键字),直接执行代码
轻量锁:存在锁之间的竞争,但竞争的会很小,
重量锁:存在资源竞争
引入java对象布局工具,用于查看对象信息
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
1.打印jvm信息
public class TestA {
private boolean a;
private int b;
}
public class TestSyncWord {
public static void main(String[] args) {
TestA a = new TestA();
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseClass(TestA.class).toPrintable(a));
}
}
得到以下结果:
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
// 对应:[Oop(Ordinary Object Pointer), boolean, byte, char, short, int, float, long, double]大小
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
com.lry.thread.TestA 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) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
从上面结果可以看出:
-
对象头(object header)占3*4=12 B
-
实例对象数据(TestA.b 和 TestA.a)占4+1=5B
-
对齐占字节7B
整个对象占24B
先看一下对象头的定义:
对象头里面的专业术语查看:
openjdk对对象头的注释
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
未使用:25位 hashcode:31位 未使用:1位 分代年龄:4位 偏向标志位:1位 对象状态:2位
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
由此可以看出一个对象头的组成
> 未使用:25位 hashcode:31位 未使用:1位 分代年龄:4位 偏向标志位:1位 对象状态:2位
java对象头在对象的不同状态下会有不同的表现形式,主要由:无所状态、加锁状态、gc标记状态。那么我们可以理解java当中的取锁其实可以理解是给对象上锁,也就是改变对象头的状态,如果上锁成功则进入同步代码块,但是java当代中的锁又分很多种,从上图可以看出大体分为偏向锁、轻量锁、重量锁三种锁状态。这三种锁的效率完全不同、关于效率的分析会在下文分析。
一个对象头有mark word 和klass pointer两个部分组成(数组对象除外,数组对象的对象头还包含一个数组长度),那么一个java的对象头有多大呢?
从源码注释中可以知道一个对象布局信息是64bit(虚拟机是64位),从上面代码解析的结果来看,对象头是12B,对象头又包含:mark word和klass pointer,mark word固定为8B(64位虚拟机是8B,32位是4B),那么klass pointer(类指针)=4B;
2. mark word分析
接下来重点分析下mark word的信息;
在无锁的情况下mark word当中的前56bit存的是对象的hashcode(在对象头信息中,前56位有25位未使用,实际是31位);
hashCode
如下程序,如果单单是打印信息是看不到hashCode迹象的,因为hashCode需要计算后才能写入,所以下面才会调用hashCode方法。
public static void main(String[] args) {
TestA a = new TestA();
System.out.println("计算前的hashcode");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
// 计算hashcode
System.out.println("16进制的hashcode:"+Integer.toHexString(a.hashCode()));
System.out.println("计算后的hashcode");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
计算前的hashcode
com.lry.thread.TestA 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) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
16进制的hashcode:5fdef03a
计算后的hashcode
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 3a f0 de (00000001 00111010 11110000 11011110) (-554681855)
4 4 (object header) 5f 00 00 00 (01011111 00000000 00000000 00000000) (95)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
要看懂上面的信息,需要引入一个概念:小端对齐。
那什么是小端对齐呢?
比如一个整数有个十百千位,那么小端对齐就是以这个顺序进行排列,左边的就是低位;
所以上面的值要反过来看:
0 4 (object header) 01 3a f0 de (00000001 00111010 11110000 11011110) (-554681855)
4 4 (object header) 5f 00 00 00 (01011111 00000000 00000000 00000000) (95)
开头的00000001不包含在hashCode的表示中,剩下:00 00 00 5f de f0 3a 前3字节,24位,没有使用。那5f de f0 3a就是他的hashcode,也和我们打印的hashcode一样,但对象头中记录hash只有31位,那么这里面有1位不在hashCode计算内。
无锁(001)
关于最后那个没有使用的字节(00000001)是对锁关系的表示:
0 0000 0 01
没有使用 分代年龄 偏向 对象状态
对象状态一共有五种,分别是:无锁、偏向锁、轻量锁、重量锁、GC标识。
到这里,打印的对象信息可以解析为:
从上一个例子(只是单纯的new出来,没有锁的情况)中可以看出,无锁状态(001);
轻量级锁(000)
这时,如果对这个对象使用synchronized(加锁)后,它的对象头会产生变化:
public static void main(String[] args) {
TestA a = new TestA();
synchronized (a) {
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 98 f0 a3 02 (10011000 11110000 10100011 00000010) (44298392)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
结果不是偏向锁,因为偏向标志位0,那么就是轻量级锁(000)
偏向锁(101)
如果:
public static void main(String[] args) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TestA a = new TestA();
synchronized (a) {
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 68 15 03 (00000101 01101000 00010101 00000011) (51734533)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
偏向标志位位1,为偏向锁,这什么情况?
一般代码中的程序都是偏向锁,因为一般程序执行都是在一个线程下执行,并不会涉及到多线程争抢锁的情况,所以jvm把所有程序都进行了偏向,所有jvm在启动时对偏向锁延迟了。
所以上面代码中会出现sleep,还可以使用下面参数来设置这个值:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
jvm也提供了其他的一些参数,我们可以使用:-XX:+PrintFlagsFinal打印jvm的一些设置信息,可以看到延迟偏向的时间是4000,当然这个值也不是准确值,他只是延迟到这个时间去触发,执行的效率我们也不知道。
加锁与解锁的过程
public class TestA {
private boolean a;
private int b;
public synchronized void count(){
b++;
}
public void count2(){
b++;
}
}
public static void main(String[] args) {
TestA a = new TestA();
System.out.println("轻量级锁 start。。。");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
synchronized (a) {
a.count2();
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
System.out.println("end ...");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
轻量级锁 start。。。
com.lry.thread.TestA 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) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) a8 f0 5a 03 (10101000 11110000 01011010 00000011) (56291496)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 1
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
end ...
com.lry.thread.TestA 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) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 1
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
资源加锁后,升级为轻量级锁,然后在释放锁后,变为无锁状态。
重量级锁(010)
public static void main(String[] args) {
TestA a = new TestA();
// 这里是无锁001
System.out.println(ClassLayout.parseInstance(a).toPrintable());
Thread t = new Thread(){
@Override
public void run() {
synchronized (a) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
a.count2();
System.out.println("sync ing--------");
}
}
};
t.start();
System.out.println("执行第一个线程(轻量级锁)---------");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
// 去除线程复用
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("睡眠1秒后(轻量级锁)--------");
// 轻量锁 000;因为没设置偏向锁的延迟时间
System.out.println(ClassLayout.parseInstance(a).toPrintable());
synchronized (a) {
System.out.println("主线程加锁(重量级锁)--------");
// 存在资源竞争,膨胀为重量级锁 010
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
System.out.println("主线程解锁后(状态为改变)--------");
// 在解锁后,标志未改变
// System.out.println(a.hashCode());
System.out.println(ClassLayout.parseInstance(a).toPrintable());
System.gc();;
System.out.println("GC 方法启动--------");
// 资源回收,无锁 001
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
com.lry.thread.TestA 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) a0 c1 00 f8 (10100000 11000001 00000000 11111000) (-134168160)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
执行第一个线程(轻量级锁)---------
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 08 f3 0a 21 (00001000 11110011 00001010 00100001) (554365704)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) a0 c1 00 f8 (10100000 11000001 00000000 11111000) (-134168160)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
睡眠1秒后(轻量级锁)--------
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 08 f3 0a 21 (00001000 11110011 00001010 00100001) (554365704)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) a0 c1 00 f8 (10100000 11000001 00000000 11111000) (-134168160)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
sync ing--------
主线程加锁(重量级锁)--------
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a 19 2e 1d (00001010 00011001 00101110 00011101) (489560330)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) a0 c1 00 f8 (10100000 11000001 00000000 11111000) (-134168160)
12 4 int TestA.b 1
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
主线程解锁后(状态为改变)--------
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a 19 2e 1d (00001010 00011001 00101110 00011101) (489560330)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) a0 c1 00 f8 (10100000 11000001 00000000 11111000) (-134168160)
12 4 int TestA.b 1
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
GC 方法启动--------
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) a0 c1 00 f8 (10100000 11000001 00000000 11111000) (-134168160)
12 4 int TestA.b 1
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
很清晰的看到:
当jvm启动后,因为短时几秒的延迟偏向,一开始是无锁状态;
执行第一个线程加锁(睡眠5秒),变为轻量级锁;
睡眠1秒后(避免线程复用);
主线程加锁(拿不到锁),变为重量级锁;
GC启动后,回收对象,清除状态;
如果,我们在启动时加上-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0(将延迟偏向时间改为0),那么一开始的打印结果就不会是无锁,而是偏向锁:101,
之后,主线程加锁时,还是拿不到锁,就会升级为重量级锁,跳过了轻量级锁。
注意:当我们调用wait方法后,偏向锁就会立即变为重量级锁。
我们修改代码:让加锁对象阻塞
public static void main(String[] args) {
TestA a = new TestA();
System.out.println("进入线程前。。。");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
Thread t = new Thread(()->{
synchronized (a) {
System.out.println("进入 线程 方法内。。");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
try {
a.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程等待结束。。。");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
});
t.start();
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("等待4秒后,主线程执行。。。");
synchronized (a) {
a.notifyAll();
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
进入线程前。。。
com.lry.thread.TestA 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) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
进入 线程 方法内。。
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 68 ad 20 (00000101 01101000 10101101 00100000) (548235269)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
等待4秒后,主线程执行。。。
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 8a 5d 4f 1d (10001010 01011101 01001111 00011101) (491740554)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
线程等待结束。。。
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 8a 5d 4f 1d (10001010 01011101 01001111 00011101) (491740554)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int TestA.b 0
16 1 boolean TestA.a false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
刚开始都是偏向锁,但有一点不同,就时第一行(对象头信息),状态后面的值不同,第一个结果是0 ,第二个的结果是非0,可以理解为第一个0的是没有线程持有,而第二个加锁后,有线程持有,偏向于加锁的这个线程。在hashcode运算之前,这种可变的状态称为可偏向状态。
注意:还有重要的一点时,当计算过hashcode后,就不能偏向了
public static void main(String[] args) {
TestA a = new TestA();
a.hashCode();
synchronized (a) {
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
没有计算前
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)
计算后的
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 c2 54 9e (00000001 11000010 01010100 10011110) (-1638612479)
4 4 (object header) 0e 00 00 00 (00001110 00000000 00000000 00000000) (14)
轻量级锁执行后就变为了无锁。
偏向锁和轻量锁性能对比
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) {
TestA a = new TestA();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000L; i++) {
synchronized (a) {
a.count2();
}
}
long endd = System.currentTimeMillis();
System.out.println("用时:"+(endd - start));
}
轻量级锁和重量级锁对比
public static void main(String[] args) throws InterruptedException {
TestA a = new TestA();
long start = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(10000000);
for (int i = 0; i < 2; i++) {
new Thread(()->{
while (latch.getCount() > 0) {
synchronized (a) {
a.count2();
latch.countDown();
}
}
}).start();
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("用时:"+(end-start));
}
得到下面的数据,偏向锁的性能,可见他们的差距非常大。
偏向 | 轻量 | 重量 |
16 | 250 | 377 |
总结
- 对象由:对象头、实例对象数据、对齐字节组成;
- 对象头占64位(32位虚拟机是32位),但只有31位表示hashcode,对象头在windows上的表示要倒着看,然后最后一位表示对象的状态;
- 加锁就是改变对象头中的标志位
- 无锁 001
- 偏向锁 101
- 轻量级锁 000
- 重量级锁 010
- 计算过hashcode后,就不能再偏向了(不会有偏向锁);
- 使用了wait方法,锁会立即变为重量级锁;
- 如果一直获取不到锁,就会升级重量级锁