轻量级锁的定位
轻量锁存在的目的是尽可能不用动用操作系统层面的互斥锁,因为那个性能会比较差。线程的阻塞和唤醒需要 CPU
从用户态转为核心态
,频繁的阻塞和唤醒对 CPU
来说是一件负担很重的工作。
如果很多对象锁的锁定状态只会持续很短
的一段时间,例如整数的自加操作,在很短的时间内阻塞并唤醒线程显然不值得,为此引入了轻量级锁。轻量级锁是一种自旋锁。
轻量级锁的获取流程
- 在抢锁线程进入临界区之前,如果轻量级锁没有被占用,JVM 首先将在抢锁线程的栈帧中建立一个锁记录(Lock Record),用于存储对轻量级锁所关联的对象
Mark Word
的拷贝。
- 抢占轻量级锁的线程使用
CAS
自旋操作,尝试将内置锁对象头的Mark Word
的ptr_to_lock_record (锁记录指针)
更新为抢锁线程栈帧中Lock Record
的地址,如果这个更新执行成功了,那么这个线程就拥有了这个对象锁。 - JVM 将
Mard Word
中的lock
标记位改为 00(轻量级锁标志), 即表示该对象处于轻量级锁状态 - JVM 会将轻量级锁关联的对象的
Mard Word
中原来的锁对象信息(如 hashcode 等),保存在抢锁线程Lock Record 的 Displaced Mark Word
这是因为锁对象的对象头此时需要存储抢占线程的LockRecord地址,没地方存储自己的hashcode :-)被占用了~~~
轻量级锁示例
还是偏向锁的Foo
对象实例,可以参考 juejin.cn/post/699440…
测试代码
@Test
public void test() throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
Foo foo = new Foo();
System.err.println(ClassLayout.parseInstance(foo).toPrintable());
foo.incr();
new Thread(new LockTest(foo)).start();
TimeUnit.SECONDS.sleep(2);
System.err.println(ClassLayout.parseInstance(foo).toPrintable());
foo.incr();
}
打印结果:
org.ywb.Foo object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf8011d21
12 4 int Foo.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
main-org.ywb.Foo object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fc2e7809005 (biased: 0x0000001ff0b9e024; epoch: 0; age: 0)
8 4 (object header: class) 0xf8011d21
12 4 int Foo.i 1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
Thread-1-org.ywb.Foo object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007000051a6918 (thin lock: 0x00007000051a6918)
8 4 (object header: class) 0xf8011d21
12 4 int Foo.i 2
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
org.ywb.Foo object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf8011d21
12 4 int Foo.i 2
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
main-org.ywb.Foo object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000070000395a898 (thin lock: 0x000070000395a898)
8 4 (object header: class) 0xf8011d21
12 4 int Foo.i 3
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
- 第一次打印的结果是对象还没有偏向任何线程,只是偏向锁
- 第二次打印的结果是线程正在执行同步代码块的时候,此时偏向锁中存储了偏向线程id和偏向时间戳
- 第三次的时候是偏向锁发生了抢占的情况,偏向锁升级成了轻量级锁
- 第四次打印的时候两个线程都执行完同步代码,此时锁被释放,对象头处于无锁状态
- 再次调用同步代码块时,锁直接变成了轻量级锁,验证了锁只能升级,不能降级。
轻量级锁的分类
普通自旋锁
所谓普通自旋锁,就是指当有线程来竞争锁时,抢锁线程会在原地循环等待,而不是被阻塞,直到那个占有锁的线程释放锁之后,这个抢锁线程就可以马上获得锁。默认情况下,自旋的次数为 10
次,用户可以通过-XX:PreBlockSpin
选项来进行更改。
因为自旋也是需要消耗CPU资源的,但是如果线程执行同步代码块的时间很长,抢占锁的线程经常耗光自己的自旋次数也没有获取到轻量级锁,最终还是被升级成重量级锁,这种情况,自旋变显得没有意义。
自适应自旋锁
自适应自旋锁,就是等待线程空循环的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数,自旋次数由前一次在同一个锁上的自旋时间
及锁的拥有者的状态
来决定。
大致原理如下:
- 如果抢锁线程在同一个锁对象上之前成功获得过的锁,那么
JVM
就会认为这次自旋也 很有可能再次成功,因此允许自旋等待持续相对更长的时间。 - 如果对于某个锁,抢锁线程在很少成功获得过,那么 JVM 将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。
JDK1.6 的轻量级锁使用的是普通自旋锁,且需要使用
-XX:+UseSpinning
选项手工开启。JDK1.7 后,轻量级锁使用自适应自旋锁,JVM 启动时自动开启,且自旋时间由 JVM 自动控制。