在前面聊过了如何使用synchronized,以及synchronized不同的加锁方式分别锁的是哪些对象。本文对synchronized底层的原理进行深层次的分析。
再前面学习了JMM之后,做为一个java程序员,肯定最大的疑问在于,一个java对象,究竟再内存中是如何存储的?因此,我们需要用到一个三方的jar包工具jol来对java对象进行查看。
导入的方式比较简单,我们只需要在pom文件中添加如下内容即可:
<!-- 查看内存布局-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
之后就可以使用jol来查看对象的内存布局了。
首先我们来查看一个Object空对象的内存布局:
public class SynchronizedTest {
public static void main(String[] args) {
Object o = new Object();
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
}
}
执行上述代码,将输出如下内容:
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
可以看到,输出结果一共有4行,输出结果分别是OFFSET表示开始的偏移量,SIZE表示大小。我们可以看到,前三行都是object header。表示对象的头文件。而前面的两行是对象头markword。第三行的4个字节是对象指针。由于该对象是一个空对象,那么最后的4个字节实际上是空的,在此只是为了对齐所用。
需要注意的是,在java中,对象指针默认是可以压缩的。我们可以用-XX:-UseCompressedClassPointers来关闭,那么此时对象指针就有8个字节。
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) 00 1c fd 1d (00000000 00011100 11111101 00011101) (503127040)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
。
在java中,数组实际上是一个特殊的对象,我们来看看数组的对象布局:
public class SynchronizedTest {
public static void main(String[] args) {
Object [] o = new Object[10];
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
}
}
其输出:
[Ljava.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) 4c 23 00 f8 (01001100 00100011 00000000 11111000) (-134208692)
12 4 (object header) 0a 00 00 00 (00001010 00000000 00000000 00000000) (10)
16 40 java.lang.Object Object;.<elements> N/A
Instance size: 56 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
可以发现,数组对象其header中会多一行,第四行,其中存的是数组的长度。在此时输出为10。
我们现在来测试将object加锁,再看看结果:
public class SynchronizedTest {
public static void main(String[] args) {
Object o = new Object();
synchronized (o) {
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
}
}
}
输出结果如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) d0 f5 e4 04 (11010000 11110101 11100100 00000100) (82114000)
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
可以看到,MarkWord明显不同于前面的情况。第一行中的值发生了明显的变化。因此,synchronized实际上是通过修改MarkWord的值来实现其加索的。 实际上这一点也非常好理解,如果需要对Object对象加锁,那么最简单的办法就是在这个对象的MarkWord上做一个标记。至于加锁的细节,我们来详细对MarkWord进行分析。
通过前面部分的内容,不难发现,再java对象中,有个关键的内容就是对象头中的MarkWord部分。 实际上,对于markWord的控制,一共有5种情况。 需要注意的是,MarkWord小端在前。 MarkWord分别对应五种状态。64bit的MarkWord如下表:
但是有的版本32位的jdk也是采用的32bit的MarkWord。
上述五种状态分别是:无锁、偏向锁、轻量级锁、重量级锁、GC回收之后的标记。 上图中的epoch,是偏向锁的时间戳。 我们再来对比之前执行的结果。 空对象的结果:
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
可以看到第一个字节的最后一位是1。为什么不是第二个字节的最后一位呢,按上表的描述,最后两个字节为01表示无锁。但是需要注意的是,jvm采用的是小端模式,数据的高字节存储再高地址中,低字节存储再低地址中。但是需要注意的是,这里每次输出的都是4个字节,再第一行的内部,jol已经帮我们做了处理。因此现在看起来第一行的最后两位才是我们上表中的锁状态位。
再synchronized的执行过程中,实际上一个对象的状态就如上表所示进行变化:
这就是synchronized锁升级的过程:
需要注意的是:
我们通过javap来看看前文中的SynchronizedTest.class的内容
$ javap -c -l SynchronizedTest
▒▒▒▒: ▒▒▒▒▒▒▒ļ▒SynchronizedTest▒▒▒▒com.dhb.test.SynchronizedTest
Compiled from "SynchronizedTest.java"
public class com.dhb.test.SynchronizedTest {
public com.dhb.test.SynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/dhb/test/SynchronizedTest;
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: aload_1
9: dup
10: astore_2
11: monitorenter
12: aload_1
13: invokestatic #3 // Method org/openjdk/jol/info/ClassLayout.parseInstance:(Ljava/lang/Object;)Lorg/openjdk/jol/info/ClassLayout;
16: invokevirtual #4 // Method org/openjdk/jol/info/ClassLayout.toPrintable:()Ljava/lang/String;
19: astore_3
20: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
23: aload_3
24: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: aload_2
28: monitorexit
29: goto 39
32: astore 4
34: aload_2
35: monitorexit
36: aload 4
38: athrow
39: return
Exception table:
from to target type
12 29 32 any
32 36 32 any
LineNumberTable:
line 9: 0
line 10: 8
line 11: 12
line 12: 20
line 13: 27
line 14: 39
LocalVariableTable:
Start Length Slot Name Signature
20 7 3 s Ljava/lang/String;
0 40 0 args [Ljava/lang/String;
8 32 1 o Ljava/lang/Object;
}
可以发现,在输出结果中,synchronized的本质,实际上是转换为了monitorenter和两个monitorexit字节码。之所以有两个字节码是因为需要对正常和异常两条路径都确保能够monitorexit退出。 monitorenter和monitorexit指令都是在hotSpot源码的objectMonitor.cpp中。后续将通过源码,对synchronized的加锁和升级过程进行分析。