偏向锁于 JDK1.6 引入,JDK15 中已被废弃,可以通过参数 -XX:+UseBiasedLocking 启用。
早期线程安全的集合如 HashTable,Vector 等都依赖于 synchronized,而大多数情况下不存在线程竞争,使用偏向锁可以减少这种情况下的同步开销,偏向锁会偏向于第一个获取它的线程。
当有其他线程尝试获取锁时偏向锁会进行撤销操作。撤销操作需要等待全局安全点,暂停持有锁的线程并检查是否在执行同步块中的代码,如果是则升级为轻量锁,否则其他线程可以竞争该锁。所以高并发的场景偏向锁会导致STW时间过长,于是在 JDK15 中被弃用。
对象头 64位虚拟机为例,偏向锁的Mark Word结构如下。
54bit
2bit
1bit
4bit
1bit
2bit
偏向线程ID
Epoch
年龄
偏向锁标志位
01
偏向锁延迟 由于 JVM 内部大量使用 synchronized 来保证线程安全而之前说过频繁的偏向锁撤销会带来额外的开销,所以 JVM 在启动后会延迟一段时间(默认为4秒)才启用偏向锁。
我们使用 jol 来打印对象头的信息,以观察偏向锁的状态。
1 2 3 4 5 <dependency > <groupId > org.openjdk.jol</groupId > <artifactId > jol-core</artifactId > <version > 0.17</version > </dependency >
新生成的对象处于无锁不偏向的状态,此时获取到的锁为轻量级锁。
延迟5秒以确保偏向锁已启用。此时新生成的对象为匿名偏向状态,偏向锁标志位为1,但存储的线程id为空。
在获取锁后偏向锁偏向当前线程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static void biasedLock () { Object lock = new Object (); System.out.println("无锁不可偏向" ); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); synchronized (lock) { System.out.println("轻量级锁" ); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } try { Thread.sleep(5000 ); } catch (InterruptedException e) { e.printStackTrace(); } lock = new Object (); System.out.println("匿名偏向" ); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); synchronized (lock) { } System.out.println("偏向当前线程" ); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); }
批量重偏向 由于多个线程频繁竞争偏向锁而产生的撤销和升级带来的性能消耗 JVM 引入了批量重偏向的机制,当一个类的对象发生偏向锁撤销的次数达到了阈值(默认20)就会触发批量重偏向。
当发生批量重偏向时首先会将对应类的 epoch 值加 1。
然后遍历处于锁定状态的对象将其对象头中的 epoch 更新为类中的值。
未被锁定状态的对象的 epoch 值则不会更新,当尝试获取锁是会发现其与类中的 epoch 值不一致,那么 JVM 就不会进行偏向锁撤销而是使用 CAS 操作将其偏向为新的线程。
下面的代码演示了批量重偏向的过程。
第19个对象因为此前偏向于线程1后被线程2持有锁,此时获取到的是轻量级锁。
第20个对象获取锁时发生了批量重偏向,获取到的为偏向锁,偏向线程2。
第1个对象在批量重偏向发生之前获取到的是轻量级锁,锁释放后处于无锁状态,且再获取锁为轻量级锁。
第25个对象在批量重偏向发生之后则处于偏向锁状态,偏向线程2。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public static void batchRebiased () { try { Thread.sleep(5000 ); } catch (InterruptedException e) { } List<Object> locks = new ArrayList <>(); for (int i = 0 ; i < 30 ; i++) { locks.add(new Object ()); } t1 = new Thread (() -> { for (Object lock : locks) { synchronized (lock) { } } LockSupport.unpark(t2); }); t2 = new Thread (() -> { LockSupport.park(); for (int i = 0 ; i < 30 ; i++) { Object lock = locks.get(i); synchronized (lock) { if (i == 18 || i == 19 ) { System.out.println("第" + (i + 1 ) + "个对象,同步中:" ); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } } if (i == 19 ) { System.out.println("第20个对象释放后:" ); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } } }); t1.start(); t2.start(); try { t2.join(); } catch (InterruptedException e) { } System.out.println("第1个对象最终状态:" ); System.out.println(ClassLayout.parseInstance(locks.get(0 )).toPrintable()); System.out.println("第25个对象最终状态:" ); System.out.println(ClassLayout.parseInstance(locks.get(24 )).toPrintable()); }
批量撤销 当竞争更次数更进一步时到达一定阈值时(默认40) JVM 会认为该类不适合使用偏向锁,触发批量撤销。
批量撤销发生后会将该类标记为不可偏向,撤销现有对象的偏向锁,处于锁定状态的对象会升级成轻量级锁,新创建的对象会处于无锁不可偏向状态。
下面代码通过多个线程竞争演示了批量撤销,批量撤销后所有对象均处于无锁不可偏向状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public static void batchRevoked () { try { Thread.sleep(5000 ); } catch (InterruptedException e) { } final List<Object> locks = new ArrayList <>(); for (int i = 0 ; i < 100 ; i++) { locks.add(new Object ()); } final int threadCount = 40 ; final CountDownLatch startLatch = new CountDownLatch (1 ); final CountDownLatch endLatch = new CountDownLatch (threadCount); for (int i = 0 ; i < threadCount; i++) { new Thread (() -> { try { startLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } for (int j = 0 ; j < 60 ; j++) { Object lock = locks.get(j); synchronized (lock) { } } endLatch.countDown(); }).start(); } startLatch.countDown(); try { endLatch.await(); } catch (InterruptedException e) { } System.out.println("第1个对象最终状态:" ); System.out.println(ClassLayout.parseInstance(locks.get(0 )).toPrintable()); System.out.println("第45个对象最终状态:" ); System.out.println(ClassLayout.parseInstance(locks.get(44 )).toPrintable()); System.out.println("批量撤销后,新创建的对象状态:" ); Object newLock = new Object (); System.out.println(ClassLayout.parseInstance(newLock).toPrintable()); }