锁
{Back to Index}
Table of Contents
1 对象头
Java 对象的对象头由 mark word
和 klass pointer
两部分组成:
- mark word 存储了同步状态,标识,hashcode ,GC 状态等
- klass pointer 存储对象的类型指针,该指针指向它的类元数据
值得注意的是,如果对象过多,使用 64 位的指针将浪费大量内存。 64 位 JVM 会默认使用选项+UseCompressedOops
开启指针压缩, 将指针压缩至32位。
Object Header (128 bits) | |||||||
Mark Word (64 bits) | Klass Word (64 bits) | ||||||
unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1(0) | lock:2(01) | OOP to metadata object | 无锁 |
thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1(1) | lock:2(01) | OOP to metadata object | 偏向锁 |
ptr_to_lock_record:62 | lock:2(00) | OOP to metadata object | 轻量锁 | ||||
ptr_to_heavyweight_monitor:62 | lock:2(10) | OOP to metadata object | 重量锁 | ||||
lock:2(11) | OOP to metadata object | GC 标记 |
- lock
锁状态标记位,该标记的值不同,整个 mark word 表示的含义不同。 - biased_lock
偏向锁标记,为 1 时表示对象启用偏向锁,为 0 时表示对象没有偏向锁。 - age
Java GC 标记位对象年龄。 - identity_hashcode
对象标识哈希码, 采用延迟加载技术 。当对象使用hashCode()
计算后,才会将结果写到该对象头中。
当对象被锁定时,该值会移动到线程 Monitor 中。 - thread
持有偏向锁的线程 ID 。
这个线程 ID 并不是 JVM 分配的线程 ID 号,和 Java Thread 中的 ID 是两个概念。 - epoch
偏向时间戳。 - ptr_to_lock_record
指向栈中锁记录的指针。 - ptr_to_heavyweight_monitor
指向线程 Monitor 的指针。
1.1 字节序说明
- 小端序
数据的高位字节存放在地址的高端 低位字节存放在地址低端 - 大端序
数据的高位字节存放在地址的低端 低位字节存放在地址高端
Figure 1: 0x01234567 的大小端字节序比较
使用 System.out.println(ByteOrder.nativeOrder())
可以查看当前 cpu 的字节序。
因为堆的地址序是从低到高的,所以 大端序更符合人类的阅读习惯。
而大部分 intel 和 amd 的 cpu 都是使用的小端序。
1.2 无锁状态
@Test public void printNoLockMemory() { MyLock lock = new MyLock(); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); System.out.println(Integer.toHexString(lock.hashCode())); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); }
com.hao.notes.jvm.MyLock object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) --- |-> 001: 无偏向,无锁 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 6b ef 00 f8 (01101011 11101111 00000000 11111000) (-134156437) ----------------------------------- |-> 压缩为 32 位的 klass pointer 12 4 int MyLock.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total 458ad742 com.hao.notes.jvm.MyLock object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 42 d7 8a (00000001 01000010 11010111 10001010) (-1965604351) -- -- -- -> 哈希值保存在这里 4 4 (object header) 45 00 b00 00 (01000101 00000000 00000000 00000000) (69) -- 8 4 (object header) 6b ef 00 f8 (01101011 11101111 00000000 11111000) (-134156437) 12 4 int MyLock.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
1.3 偏向状态
1.3.1 可偏向
JVM 启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。 在这个过程中会使用大量 synchronized 关键字对对象加锁,且这些锁大多数都不是偏向锁。 为了减少初始化时间,JVM 默认延时加载偏向锁。 这个延时的时间大概为 4s 左右,具体时间因机器而异。
可以设置 JVM 参数 -XX:BiasedLockingStartupDelay=0
来取消延时加载偏向锁。
@Test @SneakyThrows public void printBiasedLockMemory() { TimeUnit.SECONDS.sleep(5L); MyLock lock = new MyLock(); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); }
com.hao.notes.jvm.MyLock object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) --- ------------------------ |-> 101: 偏向锁 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) -----------------------------------> thread:54 全 0 8 4 (object header) 6b ef 00 f8 (01101011 11101111 00000000 11111000) (-134156437) 12 4 int MyLock.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
thread 和 epoch 的 位置的均为 0, 说明当前偏向锁并没有偏向任何线程。
此时这个偏向锁正处于可偏向状态,准备好进行偏向。可以理解为此时的偏向锁是一个 特殊状态的无锁。
1.3.2 真正偏向
@Test @SneakyThrows public void printBiasdLockMemory2() { TimeUnit.SECONDS.sleep(5L); MyLock lock = new MyLock(); synchronized (lock) { System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } }
com.hao.notes.jvm.MyLock object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 52 (00000101 00000000 00000000 01010010) (1375731717) 4 4 (object header) f2 7f 00 00 (11110010 01111111 00000000 00000000) (32754) 8 4 (object header) 6b ef 00 f8 (01101011 11101111 00000000 11111000) (-134156437) 12 4 int MyLock.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
1.4 轻量级状态
@Test @SneakyThrows public void printLightWeightLockMemory() { Thread.sleep(5000); MyLock l = new MyLock(); Thread thread1 = new Thread(){ @Override public void run() { synchronized (l){ System.out.println("thread1 locking"); System.out.println(ClassLayout.parseInstance(l).toPrintable()); } try { // thread1 退出同步代码块,但没有结束 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }; Thread thread2 = new Thread(){ @Override public void run() { synchronized (l){ System.out.println("thread2 locking"); System.out.println(ClassLayout.parseInstance(l).toPrintable()); } } }; thread1.start(); // 让thread1执行完同步代码块中方法。 Thread.sleep(4000); thread2.start(); }
thread1 locking com.hao.notes.jvm.MyLock object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 80 13 46 (00000101 10000000 00010011 01000110) (1175683077) 4 4 (object header) f4 7f 00 00 (11110100 01111111 00000000 00000000) (32756) 8 4 (object header) 25 f0 00 f8 (00100101 11110000 00000000 11111000) (-134156251) 12 4 int MyLock.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total thread2 locking com.hao.notes.jvm.MyLock object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 20 29 22 02 (00100000 00101001 00100010 00000010) (35793184) --> 00: 轻量级锁 4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672) 8 4 (object header) 25 f0 00 f8 (00100101 11110000 00000000 11111000) (-134156251) 12 4 int MyLock.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
thread1 和 thread2 交替 获取对象 A ,在 thread1 已经退出同步代码块的情况下,thread2 输出结果为轻量级锁。
1.5 重量级状态
@Test @SneakyThrows public void printHeavyLockMemory() { Thread.sleep(5000); MyLock l = new MyLock(); Thread thread1 = new Thread(){ @Override public void run() { synchronized (l){ System.out.println("thread1 locking"); System.out.println(ClassLayout.parseInstance(l).toPrintable()); try { // 让线程晚点儿终止,造成锁的竞争 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Thread thread2 = new Thread(){ @Override public void run() { synchronized (l){ System.out.println("thread2 locking"); System.out.println(ClassLayout.parseInstance(l).toPrintable()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; thread1.start(); thread2.start(); Thread.sleep(3000); }
thread1 locking com.hao.notes.jvm.MyLock object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 4a 05 81 b2 (01001010 00000101 10000001 10110010) (-1300167350) 4 4 (object header) 94 7f 00 00 (10010100 01111111 00000000 00000000) (32660) 8 4 (object header) e6 1b 01 f8 (11100110 00011011 00000001 11111000) (-134145050) 12 4 int MyLock.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total thread2 locking com.hao.notes.jvm.MyLock object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 4a 05 81 b2 (01001010 00000101 10000001 10110010) (-1300167350) 4 4 (object header) 94 7f 00 00 (10010100 01111111 00000000 00000000) (32660) 8 4 (object header) e6 1b 01 f8 (11100110 00011011 00000001 11111000) (-134145050) 12 4 int MyLock.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
2 偏向锁 1 , 2
锁会偏向于第一个执行它的线程,如果该锁后续没有其他线程访问过,那我们就不需要加锁直接执行即可。
如果后续发现了有其它线程正在获取该锁,那么会根据之前获得锁的线程的状态来决定要么将锁重新偏向新的线程,要么撤销偏向锁升级为轻量级锁。 (从上面的实验结果看出当之前获得锁的线程终止后,新的线程总是将偏向锁升级为轻量级锁,不太会发生将锁重新偏向新的线程的情况)
假设刚开始的 Mark Word 锁标识如下:
thread ID | 是否是偏向锁 | 锁标志位 |
---|---|---|
空 | 1 | 01(未被锁定) |
当线程 A (thread ID 100),去获取锁的时候,发现锁标志位为 01 ,偏向锁标志位为 1 (可以偏向),然后 CAS 将线程 ID 记录在对象头的 Mark Word,成功后 Mark Word 锁标识如下:
thread ID | 是否是偏向锁 | 锁标志位 |
---|---|---|
100 | 1 | 01(未被锁定) |
以后先 A 再次执行该方法的时候, 只需要简单的判断一下对象头的 Mark Word 中 thread ID 是否是当前线程即可, 如果是的话就直接运行。
假如此时有另外一个线程线程 B (thread ID 为 101) 尝试获取该锁,同样的去检查锁标志位和是否可以偏向的状态发现可以后, 然后 CAS 将 Mark Word 的 thread ID 指向自己,发现失败了,因为 thread ID 已经指向了线程 A ,那么此时就会去执行撤销偏向锁的操作了, 会在一个全局安全点(没有字节码在执行)去暂停拥有偏向锁的线程(A)。 并将偏向锁升级为轻量级锁,然后唤醒线程 A 执行完后续操作,线程 B 自旋 获取轻量级锁。
3 轻量级锁 3, 4
轻量级锁是使用 CAS 和自旋锁来获取锁从而降低使用操作系统互斥量来完成重量级锁的性能消耗。
当需要升级为轻量级锁时,JVM 会在 当前线程的栈帧中 创建用于存储锁记录的空间, 然后将对象头的 Mark Word 复制到锁记录中(Displaced Mark Word), 然后线程尝试使用 CAS 将对象头的 Mark Word 替换为指向锁记录的指针。
假设线程 B 替换成功,表明成功获得该锁,然后继续执行代码,此时 Mark Word 如下:
线程栈的指针 | 锁状态 |
---|---|
stack pointer 1 -> 指向线程 B | 00(轻量级锁) |
此时线程 C 来获取该锁,CAS 修改对象头的时候失败发现已经被线程 B 占用,然后它就自旋获取锁,结果线程 B 这时正好执行完成,线程 C 自旋获取成功:
线程栈的指针 | 锁状态 |
---|---|
stack pointer 2 -> 线程 C | 00(轻量级锁) |
此时线程 D 又获取该锁,发现被线程 C 占用,然后它自旋获取锁, 自旋默认 10 次后发现还是无法获得对应的锁(线程 C 还没有释放),那么线程 D 就将 Mark Word 修改为重量级锁:
线程栈的指针 | 锁状态 |
---|---|
stack pointer 2 -> 线程 C | 10(重量级锁) |
然后这时线程 C 执行完成了,将栈帧中的 Mark Word 替换回对象头的 Mark Word 的时候, 发现有其它线程竞争该锁(被线程 D 修改了锁状态)然后它释放锁并且 唤醒在等待的线程 , 后续的线程操作就全部都是重量级锁了。
线程栈的指针 | 锁状态 |
---|---|
空 | 10(重量量级锁) |
4 wait/notify/notifyAll 5
4.1 锁池和等待池
JVM 会为锁对象维护两个集合,Entry Set (锁池) 和 Wait Set (等待池):
Entry Set
如果线程 A 已经持有了对象锁,此时如果有其他线程也想获得该对象锁的话,它只能进入 Entry Set ,并且处于线程的 BLOCKED 状态
Wait Set
如果线程 A 调用了
wait()
方法,那么线程 A 会释放该对象的锁,进入到 Wait Set ,并且处于线程的 WAITING 状态
Figure 3: 锁池和等待池
需要注意的是,某个线程 B 想要获得对象锁,一般情况下有两个先决条件:
- 对象锁已经被释放了(如曾经持有锁的前任线程 A 执行完了 synchronized 代码块或者调用了
wait()
方法 - 线程 B 已处于 RUNNABLE 状态
4.2 出池
这两类 集合中的线程 都是在什么条件下可以转变为 RUNNABLE ?
对于 Entry Set 中的线程,当对象锁被释放的时候,JVM 会唤醒处于 Entry Set 中的某一个线程,这个线程的状态就从 BLOCKED 转变为 RUNNABLE 。
对于 Wait Set 中的线程,当对象的 notify()=/=notifyAll()
方法被调用时,JVM 会将处于 Wait Set 中的一个或全部线程转移到 Entry Set 中。
然后,每当对象的锁被释放后,那些所有处于 Entry Set 中的线程会共同去竞争获取对象的锁, 最终会有一个线程真正获取到对象的锁,而其他竞争失败的线程继续在 Entry Set 中等待下一次机会。
4.3 notify 和 notifyAll 的区别
设想这样的场景,两个生产者两个消费者,如果代码中使用了 notify()
而非 notifyAll()
:
假设消费者线程 1 拿到了锁,判断 buffer 为空,那么 wait()
,释放锁;
然后消费者 2 拿到了锁,同样 buffer 为空, wait()
,也就是说此时 Wait Set 中有两个线程;
然后生产者 1 拿到锁,生产,buffer 满, notify()
,那么可能消费者 1 被唤醒了,但是此时还有另一个线程生产者 2 在 Entry Set 中盼望着锁,
并且最终抢占到了锁,但因为此时 buffer 是满的,因此它要 wait()
;
然后消费者 1 拿到了锁,消费, notify()
;
这时就有问题了,此时生产者 2 和消费者 2 都在 Wait Set 中,buffer 为空,如果唤醒生产者2,没有问题;
但如果唤醒了消费者 2,因为 buffer 为空,它会再次 wait()
,这就尴尬了,
万一生产者 1 已经退出不再生产了,没有其他线程在竞争锁了,只有生产者 2 和消费者 2 在 Wait Set 中互相等待,这样就产生死锁了。
如果把上述例子中的 notify()
换成 notifyAll()
,这样的情况就不会出现了,因为每次 notifyAll()
都会使其他等待的线程从 Wait Set 进入 Entry Set,从而都有机会获得锁。
尽量使用 notifyAll() 的原因就是,notify() 非常容易导致死锁。