偏向锁可以认为是在没有多线程竞争的情况下访问synchronized修饰的代码块的加锁场景,也就是单线程执行的情况下。
偏向锁的作用就是,在没有线程竞争的情况下去访问synchronized同步代码块时,会尝试先通过偏向锁来抢占访问资格,这个抢占过程是基于CAS来完成的,如果抢占锁成功,则直接修改对象头中的锁标记。其中,偏向锁标记为1,锁标记为01,以及存储当前获得锁的线性ID。而偏向锁的意思就是,如果线程X获得了偏向锁,那么当线程X后续再访问这个同步方法时,只需要判断对象中的线程ID和线程X是否相等即可。如果相等,就不需要再次去抢占锁,直接获得访问资格即可。
public class BiasedLockExample { public static void main(String[] args) { BiasedLockExample example = new BiasedLockExample(); System.out.println("加锁之前"); System.out.println(ClassLayout.parseInstance(example).toPrintable()); synchronized (example){ System.out.println("加锁之后"); System.out.println(ClassLayout.parseInstance(example).toPrintable()); } } } //运行结果 加锁之前 com.example.jol.demo.BiasedLockExample 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 加锁之后 com.example.jol.demo.BiasedLockExample object internals: OFFSET SIZE TYPE DEscriptION VALUE 0 4 (object header) b8 f1 0e 03 (10111000 11110001 00001110 00000011) (51311032) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
- 在加锁之前,对象头中的第一个字节00000001最后三位为001,其中低位的两位表示锁标记,它的值是01,表示当前为无锁状态。
- 在加锁之后,对象头中的第一个字节10111000,最后三位为000,其中低位的两位是00,对照前面的Mark Word中的存储结构的含义,它表示轻量级锁状态。
注意:JVM在启动的时候,有一个启动参数-XX:BiasedLockingStartupDelay,这个参数表示偏向锁延迟开启的时间,默认是4秒,也就是说在我们运行上述程序时,偏向锁还未开启,导致最终只能获得轻量级锁。之所以延迟启动,是因为JVM在启动的时候会有很多线程运行,也就是说会存在线程竞争的场景,那么这时候开启偏向锁的意义不大。
想要看到偏向锁实现效果,有两种方法:
- 添加JVM启动参数-XX:BiasedLockingStartupDelay=0,把延迟启动时间设置为0。
- 抢占锁资源之前,先通过Thread.sleep()方法睡眠4秒以上。
加锁之前 com.example.jol.demo.BiasedLockExample object internals: OFFSET SIZE TYPE DEscriptION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 加锁之后 com.example.jol.demo.BiasedLockExample object internals: OFFSET SIZE TYPE DEscriptION VALUE 0 4 (object header) 05 38 e0 02 (00000101 00111000 11100000 00000010) (48248837) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
从上面输出结果发现,加锁之后,第一个字节低三位变成了101,高位1表示当前是偏向锁状态,低位01表示当前是偏向锁状态。但是加锁之前的锁标记位也是101。这是为什么呢?
仔细看一下会发现,加锁之前并没有存储线程ID,加锁之后才有一个线程ID(48248837)。因此。在获得偏向锁之前,这个标记表示是可偏向状态,并不代表已经处于偏向锁状态。
轻量级锁原理所谓的轻量级锁,就是没有抢占到锁的线程,进行一定次数的重试(自旋)。比如线程第一次没抢到锁则重试几次,如果在重试的过程中抢占到了锁,那么这个线程就不需要阻塞,这种实现方式我们称之为自旋锁。
线程通过重试来抢占锁的方式是有代价的,因为线程如果不断的自旋重试,那么CPU会一直处于运行状态。如果持有锁的线程占有锁的时间比较短,那么自旋等待的实现带来的性能提升会比较明显。反之,如果持有锁的线程占用锁资源的时间比较长,那么自旋的线程就会浪费CPU资源,所以线程重试抢占锁的次数必须要有一个限制。
在JDK1.6中默认的自旋锁次数是10,可以通过-XX:PreBlockSpin参数来调整自旋锁次数。同时开发者在JDK1.6中还对自旋锁做了优化,引入了自适应自旋锁,自适应自旋锁的自旋次数不是固定定的,而是根据前一次在同一个锁上的自旋次数及锁持有者的状态来决定的。如果在同一个锁对象上,通过自旋等待成功获得过锁,并且持有锁的线程正在运行中,那么JVM会认为此次自旋也有很大的机会获得锁,因此会将这个线程的自旋时间延长。反之,如果在一个锁对象中,通过自旋锁获得锁很少成功,那么JVM会缩短自旋次数。
public class BiasedLockExample { public static void main(String[] args) { BiasedLockExample example = new BiasedLockExample(); System.out.println("加锁之前"); System.out.println(ClassLayout.parseInstance(example).toPrintable()); synchronized (example){ System.out.println("加锁之后"); System.out.println(ClassLayout.parseInstance(example).toPrintable()); } } } //运行结果 加锁之前 com.example.jol.demo.BiasedLockExample 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 加锁之后 com.example.jol.demo.BiasedLockExample object internals: OFFSET SIZE TYPE DEscriptION VALUE 0 4 (object header) b8 f1 0e 03 (10111000 11110001 00001110 00000011) (51311032) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total重量级锁的原理
如果没有抢占到锁资源的线程通过一定次数的自旋锁后,发现仍然没有获得锁,就只能阻塞等待了,所以最终会升级到重量级锁,通过系统层面的互斥量来抢占锁资源。
如果偏向锁、轻量级锁这些类型中无法让线程获得锁资源,那么这些没获得锁的线程最终的结果仍然是阻塞等待,直到获得锁的线程释放锁之后才能被唤醒。而在整个优化过程中,我么通过乐观锁的机制来保证线程安全性。
public class HeavyLockExample { public static void main(String[] args) throws InterruptedException { HeavyLockExample heavyLockExample = new HeavyLockExample(); System.out.println("加锁之前"); System.out.println(ClassLayout.parseInstance(heavyLockExample).toPrintable()); Thread thread = new Thread(()->{ synchronized (heavyLockExample){ try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); TimeUnit.MICROSECONDS.sleep(500); System.out.println("thread线程抢占了锁"); System.out.println(ClassLayout.parseInstance(heavyLockExample).toPrintable()); synchronized (heavyLockExample){ System.out.println("main线程来抢占锁"); System.out.println(ClassLayout.parseInstance(heavyLockExample).toPrintable()); } } } //运行结果 加锁之前 com.example.jol.demo.HeavyLockExample 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total thread线程抢占了锁 com.example.jol.demo.HeavyLockExample object internals: OFFSET SIZE TYPE DEscriptION VALUE 0 4 (object header) 98 f4 3a 20 (10011000 11110100 00111010 00100000) (540734616) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total main线程来抢占锁 com.example.jol.demo.HeavyLockExample object internals: OFFSET SIZE TYPE DEscriptION VALUE 0 4 (object header) 6a 1d d8 1c (01101010 00011101 11011000 00011100) (483925354) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
- 加锁之前,对象头中的第一个字节是00000001,表示无锁状态。
- 当thread线程去抢占锁时,对象头中的第一个字节变成了11011000,表示轻量级锁状态。
- 接着main线程来抢占统一对象锁,由于thread线程睡眠了2秒,此时锁还没有被释放main线程无法通过轻量级锁自旋获得锁,因此它的锁类型是重量级锁,锁标记为10。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)