先来引入锁的概念:
偏向锁:当前只有一个锁,无线程竞争的情况下,尽量减少不必要的轻量锁的执行路径。
偏向锁就是在运行过程中,对象的锁偏向某个线程,即在开启偏向锁的情况下,某个线程获得锁,当该线程下次想要获得锁时,不需要再获取锁(忽略synchronized关键字),直接执行代码
轻量锁:存在锁之间的竞争,但竞争的会很小,
重量锁:存在资源竞争
openjdk对对象头的注释
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// 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)
一个对象包括:
java对象头在对象的不同状态下会有不同的表现形式,主要由:无所状态、加锁状态、gc标记状态。那么我们可以理解java当中的取锁其实可以理解是给对象上锁,也就是改变对象头的状态,如果上锁成功则进入同步代码块,但是java当代中的锁又分很多种,从上图可以看出大体分为偏向锁、轻量锁、重量锁三种锁状态。这三种锁的效率完全不同、关于效率的分析会在下文分析。
引入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
从上面结果可以看出:
整个对象占24B
对象头里面的专业术语查看:
http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object's layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format. 每个GC管理的堆对象开头的通用结构。 (每个oop都指向一个对象标头。)包括有关堆对象的布局,类型,GC状态,同步状态和标识哈希码的基本信息。 由两个词组成。 在数组中,紧随其后的是长度字段。 请注意,Java对象和VM内部对象都具有通用的对象标头格式。 klass pointer The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the "klass" contains a C++ style "vtable". 每个对象标头的第二个字。 指向另一个对象(元对象),该对象描述原始对象的布局和行为。 对于Java对象,“容器”包含C ++样式“ vtable”。 mark word The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits. 每个对象标头的第一个单词。 通常,一组位域包括同步状态和标识哈希码。 也可以是指向与同步相关的信息的指针(具有特征性的低位编码)。 在GC期间,可能包含GC状态位。
可知一个对象头有mark word 和klass pointer两个部分组成(数组对象除外,数组对象的对象头还包含一个数组长度),那么一个java的对象头有多大呢?
从源码注释中可以知道一个mark word 是64bit,从上面代码解析的结果来看,对象头是12B,mark word固定为8B,那么klass pointer=4B;
接下来重点分析下mark word的信息;
在无锁的情况下mark word当中的前56bit存的是对象的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
这里引入一个概念:小端对齐,那什么是小端对齐呢?
高内存地址放整数的高位,低内存地址放整数的低位,就叫小端对齐,Windows都是这样的。
从上面计算hashcode后的打印可以看到它的对象头发生变化;上面对象头中说有25位没有使用,31为是hashcode,剩下的对象的状态表示。
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,剩下:00 00 00 5f de f0 3a 前3位,24位,没有使用。那5f de f0 3a就是他的hashcode,也和我们打印的hashcode一样。
那最后那个没有使用的字节是怎样的表示呢?
0 0000 0 01 没有使用 分代年龄 偏向 对象状态
对象状态一共有五种,分别是:无锁、偏向锁、轻量锁、重量锁、GC标识。
从上一个例子中可以看出,无锁状态表示是001
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)
如果:
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在启动时对偏向锁延迟了,在启动后再加上锁,所有才会出现上面代码sleep,还可以使用下面参数来设置这个值。
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
jvm也提供了其他的一些参数,我们可以使用:-XX:+PrintFlagsFinal打印jvm的一些设置信息,可以看到延迟偏向的时间是4000,当然这个值也不是准确值,他只是延迟到这个时间去触发,执行的效率我们也不知道。
加锁于解锁的过程
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
资源加锁后,升级为轻量级锁,然后在释放锁后,变为无锁状态。
重量级锁的情况:
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("线程t 启动后,主线程睡眠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(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) d8 ef 4e 20 (11011000 11101111 01001110 00100000) (542044120)
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
线程t 启动后,主线程睡眠1秒--------
com.lry.thread.TestA object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) d8 ef 4e 20 (11011000 11101111 01001110 00100000) (542044120)
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) ea 12 79 1c (11101010 00010010 01111001 00011100) (477696746)
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) ea 12 79 1c (11101010 00010010 01111001 00011100) (477696746)
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秒后,主线程取,发现现在资源被线程t加了锁,这时对象状态还未改变,当主线程sync调用a.方法后,发现a对象资源正在被线程t加锁中,所以这时候就出现资源竞争,变为重量级锁。
如果,我们在启动时加上-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=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 |
总结: