前言:
上一篇分析了Synchronized关键字的用法详解,本篇则对Synchronized关键字对底层实现原理进行详细分析。
Synchronized关键字是并发编程中的元老级别角色,因此被人们习惯性称为“重量级锁”,随着Java SE1.6的优化,Synchronized引入了偏向锁和轻量级锁,则显得没有那么重了。
上文提到Synchronized共有三种用法,而锁的是每种用法对应的对象:
1.修饰非静态方法,锁的是当前对象
2.修饰静态方法,锁的是当前类的Class对象
3.修饰代码块,锁的是括号内传入的对象
那么Synchronized关键字到底是怎么将这些对象锁起来的呢?又是怎么解锁的呢?
一、锁的本质
Java中每个对象都会有一个Monitor对象与之关联,当获取到这个monitor对象后,与之对应的对象就被锁住。Synchronized关键字在修饰代码块时,编译之后,会在代码块指令的前后分别加上monitorenter和monitorexit指令,这两个指令的意思分别就是进入和退出Monitor对象。也就是说monitorenter就是获取锁,monitorexit就是释放锁。而修饰方法的时候是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。接下来就以一段代码为例,看下Synchronized编译之后的样子。
Demo代码如下:
1 public class SynchronizedDemo { 2 3 public static Integer i = 0; 4 5 public synchronized void test1(){ 6 System.out.println("这是修饰非静态方法"); 7 } 8 9 10 public synchronized static void test2(){ 11 System.out.println("这是修饰静态方法"); 12 } 13 14 public void test3(){ 15 synchronized (i){ 16 System.out.println("这是修身代码块"); 17 } 18 } 19 }
定义了三个方法,分别使用了三种用法的Synchronized关键字,接下来再看下编译后的字节码是什么样子的,如下:
1 Compiled from "SynchronizedDemo.java" 2 public class com.lucky.study.concurrent.SynchronizedDemo { 3 public static java.lang.Integer i; 4 5 public com.lucky.study.concurrent.SynchronizedDemo(); 6 Code: 7 0: aload_0 8 1: invokespecial #1 // Method java/lang/Object."<init>":()V 9 4: return 10 11 public synchronized void test1(); 12 Code: 13 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 14 3: ldc #3 // String 这是修饰非静态方法 15 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 16 8: return 17 18 public static synchronized void test2(); 19 Code: 20 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 21 3: ldc #5 // String 这是修饰静态方法 22 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 23 8: return 24 25 public void test3(); 26 Code: 27 0: getstatic #6 // Fiel
d i:Ljava/lang/Integer; 28 3: dup 29 4: astore_1 30 5: monitorenter 31 6: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 32 9: ldc #7 // String 这是修身代码块 33 11: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 34 14: aload_1 35 15: monitorexit 36 16: goto 24 37 19: astore_2 38 20: aload_1 39 21: monitorexit 40 22: aload_2 41 23: athrow 42 24: return 43 Exception table: 44 from to target type 45 6 16 19 any 46 19 22 19 any 47 48 static {}; 49 Code: 50 0: iconst_0 51 1: invokestatic #8 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 52 4: putstatic #6 // Field i:Ljava/lang/Integer; 53 7: return 54 }
二、对象头和Monitor对象
在JVM中,每个对象在内存中主要分为三个部分:对象头、实例数据和填充数据,而Synchronized关键字使用的锁对象是存放在对象的对象头中。JVM会用2个字宽来存储对象头,如果对象是数组类型,则用3个字来存,多出的1个字用来单独存数组的长度。1个字宽相当于4个字节,相当于32bit。
长度 | 内容 | 备注 |
32/64bit | Mark Word | 存储对象的hashCode、锁信息和分代信息等 |
32/64bit | Class Metadata Address | 对象类型指针,指向当前对象是哪个类 |
32/64bit | Array length | 数组长度 (非数组对象则没有) |
对象头的默认存储结构如下:
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标志位 |
无锁状态 | 对象的hashcode | 对象分代年龄 | 0 | 01 |
由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构。
对象头除了存储Mark Word之外还会存储Klass pointer,也就是类的Class对象的指针。
Mark Word存储同步状态、标识、hashCode、GC状态等;Klass pointer指向对象的类元信息
以64位为例,各种情况下对象头的结构分别如下:
|--------------------------------------------------------------------------------------------------------------|
| Object Header (128 bits) |
|--------------------------------------------------------------------------------------------------------------|
| Mark Word (64 bits) | Klass Pointer (64 bits) |
|--------------------------------------------------------------------------------------------------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 无锁
|----------------------------------------------------------------------|--------|------------------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 偏向锁
|----------------------------------------------------------------------|--------|------------------------------|
| ptr_to_lock_record:62 | lock:2 | OOP to metadata object | 轻量锁
|----------------------------------------------------------------------|--------|------------------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | 重量锁
|----------------------------------------------------------------------|--------|------------------------------|
| | lock:2 | OOP to metadata object | GC
|--------------------------------------------------------------------------------------------------------------|
biased_lock | lock | 锁类型 |
0 | 01 | 无锁 |
1 | 01 | 偏向锁 |
0 | 00 | 轻量级锁 |
0 | 10 | 重量级锁 |
0 | 11 | GC标记 |
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version </dependency>
打印代码如下:
System.out.println(ClassLayout.parseInstance(SyncTest.class).toPrintable());
新建User类,包含一个Long userId字段,新建User类实例,打印对象头
public static void main(String[] args){ User user = new User(); System.out.println(ClassLayout.parseInstance(user).toPrintable()); }
结果如下:
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope com.lucky.test.jvm.User 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 java.lang.Long User.userId null Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
第一行:00000001 依次表示 unused占1位0,age占4位0,biased_lock占1位0,lock占2位01,此时表示无锁状态
第三行:表示User.class对象的指针
第四行:表示User对象的userId属性,值为空
第五行:对象的大小,占用16个字节,其中Mark Word占用8个字节,Class Pointer经过压缩占用4个字节,long类型属性占有4个字节,共16个字节
tips:如果将User类的userId属性去掉会占有多个字节呢?会是12个字节码?答案也是16个字节
结果如下:
com.lucky.test.jvm.User 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 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
虽然没有了userId字段,但是会进行字节填充4个字节,因为Java给对象分配内存是是以8个字节为单位的,所以如果对象实际占有不足8的倍数,就是进行字节填充到8的倍数,而对象头占有12个字节,所以即使是空对象也至少需要占有16个字节。
言归正传,当对这个对象添加偏向锁时,对象头部就会发生变化,结果如下:
public static void main(String[] args) throws InterruptedException { Thread.sleep(5000L); User user = new User(); synchronized (user) { System.out.println(ClassLayout.parseInstance(user).toPrintable()); } }
com.lucky.test.jvm.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 d0 00 1e (00000101 11010000 00000000 00011110) (503369733) 4 4 (object header) e6 7f 00 00 (11100110 01111111 00000000 00000000) (32742) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
第一行:00000101 表示biased_lock标记为1,lock标记为01,此时对象的状态为偏向锁,后面存储的是持有偏向锁的线程信息
轻量级锁
代码如下:
public static void main(String[] args) throws InterruptedException { Thread.sleep(5000L); User user = new User(); Thread thread = new Thread(new Runnable() { @Override public void run() { synchronized (user) { System.out.println("子线程偏向锁"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } } }); thread.start(); thread.join(); Thread.sleep(10000L); synchronized (user) { System.out.println("主线程轻量级锁"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } }
打印结果如下:
1 子线程偏向锁 2 # WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope 3 com.lucky.test.jvm.User object internals: 4 OFFSET SIZE TYPE DESCRIPTION VALUE 5 0 4 (object header) 05 e8 83 c0 (00000101 11101000 10000011 11000000) (-1065097211) 6 4 4 (object header) c1 7f 00 00 (11000001 01111111 00000000 00000000) (32705) 7 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 8 12 4 (loss due to the next object alignment) 9 Instance size: 16 bytes 10 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 11 12 主线程轻量级锁 13 com.lucky.test.jvm.User object internals: 14 OFFSET SIZE TYPE DESCRIPTION VALUE 15 0 4 (object header) c8 98 ff 0b (11001000 10011000 11111111 00001011) (201300168) 16 4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672) 17 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 18 12 4 (loss due to the next object alignment) 19 Instance size: 16 bytes 20 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看出子进程获取到锁的时候是偏向锁,主进程等子进程全部执行完成后获取锁就变成了轻量级锁,即使此时已经没有锁竞争情况了,还是会将锁升级为轻量级锁
重量级锁
代码如下:
public static void main(String[] args) throws InterruptedException { Thread.sleep(5000L); User user = new User(); Thread thread1 = new Thread(new Runnable() { @SneakyThrows @Override public void run() { synchronized (user) { System.out.println("子线程1获取锁"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); Thread.sleep(2000L); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { synchronized (user) { System.out.println("子线程2获取锁"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } } }); thread1.start(); Thread.sleep(1000L); thread2.start(); }
流程为线程1先获取锁,此时为偏向锁,然后线程2尝试获取锁和线程1存在竞争关系,将锁升级为重量级锁,所以线程1睡眠2000之后就会打印重量级锁信息,打印结果如下:
1 子线程1获取锁 2 # WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope 3 com.lucky.test.jvm.User object internals: 4 OFFSET SIZE TYPE DESCRIPTION VALUE 5 0 4 (object header) 05 18 24 7b (00000101 00011000 00100100 01111011) (2065963013) 6 4 4 (object header) a2 7f 00 00 (10100010 01111111 00000000 00000000) (32674) 7 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 8 12 4 (loss due to the next object alignment) 9 Instance size: 16 bytes 10 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 11 12 com.lucky.test.jvm.User object internals: 13 OFFSET SIZE TYPE DESCRIPTION VALUE 14 0 4 (object header) ea 91 01 7d (11101010 10010001 00000001 01111101) (2097254890) 15 4 4 (object header) a2 7f 00 00 (10100010 01111111 00000000 00000000) (32674) 16 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 17 12 4 (loss due to the next object alignment) 18 Instance size: 16 bytes 19 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 20 21 子线程2获取锁 22 com.lucky.test.jvm.User object internals: 23 OFFSET SIZE TYPE DESCRIPTION VALUE 24 0 4 (object header) ea 91 01 7d (11101010 10010001 00000001 01111101) (2097254890) 25 4 4 (object header) a2 7f 00 00 (10100010 01111111 00000000 00000000) (32674) 26 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 27 12 4 (loss due to the next object alignment) 28 Instance size: 16 bytes 29 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
线程1第一次打印为偏向锁,第二次打印和线程2打印的锁信息均是重量级锁。
三、JVM对Synchronized锁的优化
Java SE1.6为了减轻获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,也就有了从低到高为:无锁、偏向锁、轻量级锁和重量级锁四种锁的状态。锁可以从低到高升级,但是不能降级。
偏向锁:
大多数情况下,锁不存在多线程竞争,而是由同一个线程多次获得,而同一个线程对于同一个对象需要频繁的获取锁和释放锁,无疑是浪费了很多资源,因此就有了偏向锁的引入。
偏向锁的核心思想是如果一个线程获取到了锁,那么锁就有了偏向性,对象头中的Mark Word就变为偏向锁结构。当这个线程再次获取锁的时候,就不需要再通过CAS操作来争取锁,而可以直接获取锁。也就省去了大量的申请锁的过程。
所以针对锁竞争不严重,经常由同一个线程获取锁的情况下,偏向锁有了很明显的优化效果。但是如果锁竞争比较严重的情况,每次获取锁的线程都是不同的情况,偏向锁则失去了意义,这时候偏向锁会进行升级,升级成轻量级锁。
偏向锁撤销:偏向锁一旦被一个线程获取之后,对象头中就会记录锁偏向的线程信息,即使线程执行完成且锁已经被释放了也不会清除对象头中的线程信息。而一旦有其他线程来竞争锁时,偏向锁就需要进行撤销。撤销的过程需要等待全局安全点,也就是当前持有偏向锁的线程在这个时间点上没有字节码在执行的时机。它会先暂停持有偏向锁的线程,然后检查持有偏向锁的线程是否还存活,如果线程已经不存活了,那么就将对象头置为无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
轻量级锁:
重量级锁:
通过对象内部监视器(monitor)实现,monitor本质前面也提到了是基于操作系统互斥(mutex)实现的,操作系统实现线程之间切换需要从用户态到内核态切换,成本非常高
为什么只能升级而不能降级呢?
因为一旦锁升级就表示存在锁竞争,而存在锁竞争的情况下,轻量级锁和偏向锁的效率并不高,偏向锁需要频繁修改对象头,轻量级锁需要一直自旋来尝试获取锁,都没有直接阻塞线程对CPU更加友好。