偏向锁理论太抽象,实战了解下偏向锁如何发生以及如何升级【实战篇】

锁升级

  • 上文我们主要介绍什么是偏向锁,轻量级锁,重量级锁。并分析了三者的区别和使用场景。还记得Redis章节中整数集中升级操作吗。在锁中我们同样是设计锁升级和降级的。上文我们也介绍了当没有竞争时偏向锁,出现竞争时就轻量级锁。
  • 但是轻量级锁时cas操作和自旋等待。自旋只能适合并发少的情况,如果并发很多一个线程可能需要等待很久才能获取到锁,那么自旋期间的开销也是很巨大的,所以就很有必要升级轻量级锁。那么什么时候该升级重量级锁呢?JVM中也是设置了自旋次数的,超过一定次数就会发生升级成重量级锁

偏向锁升级轻量级锁

  • 个人认为重点还是偏向锁升级的过程。因为偏向锁不会主动撤销,所以锁升级过程涉及批量锁撤销,批量锁偏向等场景。

image-20211213152554303.png

  • 还记得偏向锁在锁对象的markword中的存储结构吗,末尾三位是101表示偏向锁。关于Lock Record就是上面我们提到的线程栈顶的锁记录对象的指针,关于锁记录内部存储了整个锁对象的markword , 而这里我们需要注意的是EPOCH , EPOCH翻译过来是纪元的意思。我们简单理解成版本好
  • 说到版本号,我们还得熟悉JVM关于偏向锁的两个属性设置

image-20211213153045152.png

  • 发生轻量级锁升级的时候就会发生偏向锁的撤销。如果JVM发现某一类锁发生锁撤销的次数大于等于-XX:BiasedLockIngBulkRebiasThreshold=20时,就会宣布偏向锁失效。让偏向锁失效就是将版本号加1 即 EPOCH+1;
  • 当一个类锁发生的总撤销数大于等于-XX:BiasedLockingBulkRevokeThreshold=40,则后续在上锁会默认上轻量级锁。
class Demo{
    String userName;
}
public class LockRevoke {
    public static void main(String[] args) throws InterruptedException {
        List<Demo> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            list.add(new Demo());
        }
        final Thread t1 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                for (int i = 0; i < 99; i++) {
                    Demo demo = list.get(i);
                    synchronized (demo) {
                    }
                }
                TimeUnit.SECONDS.sleep(100000);
            }
        });

        final Thread t2 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                synchronized (list.get(99)) {
                    System.out.println("第100个对象上锁中,并持续使用该对象" + ClassLayout.parseInstance(list.get(99)).toPrintable());
                    TimeUnit.SECONDS.sleep(99999);
                }
            }
        });

        final Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    Demo demo = list.get(i);
                    synchronized (demo) {


                        if (i == 18) {
                            System.out.println("发送第19次锁升级,list.get(18)应该是轻量级锁" + ClassLayout.parseInstance(list.get(18)).toPrintable());
                        }
                        if (i == 19) {
                            System.out.println("发送第20次锁升级,会发生批量重偏向;纪元+1;后续偏向锁都会偏向当前线程;list.get(19)应该是轻量级锁" + ClassLayout.parseInstance(list.get(19)).toPrintable());
                            System.out.println("因为第100对象仍然在使用,需要修改起纪元" + ClassLayout.parseInstance(list.get(99)).toPrintable());
                        }
                        if (i == 29) {
                            System.out.println("在批量重偏向之后;因为第一次偏向锁已经失效了,所以这里不是轻量级而是偏向该线程的偏向锁" + ClassLayout.parseInstance(list.get(29)).toPrintable());
                        }
                        if (i == 39) {
                            System.out.println("发送第40次锁升级,发生批量锁撤销;这里应该是轻量级锁后续都是轻量级" + ClassLayout.parseInstance(list.get(39)).toPrintable());
                        }
                    }
                }

            }
        });
        
        t1.start();
        t2.start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println("第一次上锁后list.get(0)应该偏向锁:" + ClassLayout.parseInstance(list.get(0)).toPrintable());
        System.out.println("第一次上锁后list.get(19)应该偏向锁:" + ClassLayout.parseInstance(list.get(19)).toPrintable());
        System.out.println("第一次上锁后list.get(29)应该偏向锁:" + ClassLayout.parseInstance(list.get(29)).toPrintable());
        System.out.println("第一次上锁后list.get(39)应该偏向锁:" + ClassLayout.parseInstance(list.get(39)).toPrintable());
        System.out.println("第一次上锁后list.get(99)应该偏向锁:" + ClassLayout.parseInstance(list.get(99)).toPrintable());
        t3.start();
       

    }

}
  • 上面就是典型的偏向锁重偏向和偏向锁撤销案列整合。
  • 首先我们t1线程率先将前99个对象都上锁并立马释放,因为我们的vm设置取消偏向锁延迟了,如何设置请看文章开头部分。
  • 第2个线程t2只对最后一个对象进行上锁,不同的是上锁后永久占着不释放。那么别人就无法获取到最后一个对象的锁
  • 第3个线程开始和上面初始化好的对象进行抢占资源。第三个线程只循环了40次,因为JVM默认的最大撤销偏向锁次数就是40次。后面都是轻量级锁了。
  • 因为第3个线程会发生批量重偏向,所以后续不会造成偏向锁撤销。如果像看到批量锁撤销,就必须在开一个线程上锁。所以线程4就是继续造成撤销,但是要保证线程4后执行,否则t3,t4同时执行会造成重量级锁,因为重量级锁的场景之一就是:1个偏向锁,1个轻量级锁,1个正在请求就会出发重量级锁
  • 在第三个线程中对i==18即第19个元素进行上锁时,因为之前已经被上了偏向锁,虽然被释放了锁,但是偏向锁本身并不会释放,这个前面也已经铺垫了。所以此时第19个元素先发生锁撤销,然后在上轻量级锁。所以这里预测第19个对象时轻量级锁
  • 然后来到i19,即第20个元素,因为JVM默认类总撤销大于等于20会发生批量重偏向。啥意思呢?在t3 中i19之前上锁都是轻量级。i19之后在上锁就会时偏向锁,只不过是偏向线程3的,而不是偏向线程1的。这里我们可以和第一次的i19内存布局进行对比,除了线程id不一样还有一个纪元不一样,

image-20211213161634774.png

  • 上面为什么我会单独起一个线程锁定list.get(99)呢?就是为了测试当发生批量重偏向的时候能够直观看到正在使用的锁纪元信息被修改,以免造成锁丢弃

image-20211213162536276.png

  • 我们能够看的出来在发生批量重偏向的时候,正在使用的锁纪元信息会被更新,如果不更新会被JVM认为是废弃偏向锁。当然发生批量重偏向后再次获取对象锁就不会在发生锁撤销了。因为之前的锁已经废弃了,所以我们获取一下后续的锁信息,这里就看看list.get(29)吧。

image-20211213162904537.png

  • 第4个线程在第三个线程之后不断造成撤销,将达到撤销总数40的时候,JVM就会认为后续该类的锁不适合做偏向锁了,直接就是轻量级锁

image-20211213164055601.png

页面下部广告