多线程五 锁的膨胀过程

目录

1.偏向锁发生情况?

线程复用 

批量重偏向

批量撤销

2.轻量级锁什么时候发生的?

总结



上一篇中,主要是对加解锁,还有锁的分类有了一个了解:

1. 锁在对象中的表示(无锁:001  ,偏向锁:101 ,轻量级锁: 000,重量级锁:  010)

2. 锁分:偏向锁、轻量级锁、重量级锁

3. jvm在启动时会有大概4秒的延迟偏向,所以,一开始程序运行是无锁状态,可以通过run时加入参数修改这个值

4. 偏向锁偏向锁在计算hashCode后不能在偏向

5. 调用wait方法后,直接升级为重量级锁

6. 长时间获取不到锁会升级重量级锁

1.偏向锁发生情况?

    上一篇中,我们发现jvm的优化过程中,存在延迟偏向,我们通过让线程睡眠过了延迟时间之后,锁从一开始的轻量级锁变为偏向锁(也可以设置参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0,将延迟时间设置为0,),来查看单线程对锁的作用。

再者可以这样理解:

在加锁的情况下,多个线程会竞争资源,但是在后续的程序运行中,几乎都是同一个线程获取锁,所以,为了避免加锁解锁的资源浪费,将锁偏向为一个线程,提高同步代码的性能。

线程复用 

还有一种情况:

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 

public static void main(String[] args) {
    TestA a = new TestA();
    Thread t1 = new Thread(){
        @Override
        public void run() {
            synchronized (a) {
                System.out.println("第一个线程。。。。");
                System.out.println(ClassLayout.parseInstance(a).toPrintable());
            }
        }
    };
    t1.start();

    try {
        t1.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    Thread t2 = new Thread(){
        @Override
        public void run() {
            synchronized (a) {
                System.out.println("第二个线程。。。。");
                System.out.println(ClassLayout.parseInstance(a).toPrintable());
            }
        }
    };
    t2.start();
}

看结果前,不妨猜测一下,线程t2是不是轻量级锁?

第一个线程。。。。
com.lry.thread.TestA object internals:
OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           05 b8 ab 1f (00000101 10111000 10101011 00011111) (531347461)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4       int TestA.b                                   0
     16     1   boolean TestA.a                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total


第二个线程。。。。
com.lry.thread.TestA object internals:
OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           05 b8 ab 1f (00000101 10111000 10101011 00011111) (531347461)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4       int TestA.b                                   0
     16     1   boolean TestA.a                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

结果是两个都是偏向锁,首先他不存在资源竞争,交互获取,不看结果真不知道是偏向锁,而且两个线程中的线程ID(531347461 这里他不是确切的线程ID,但可以当做是)也是一样的,这里出现一个线程复用的问题,第一个线程被销毁后,第二个线程会拿到同样的ID,然后同步操作时会判断线程ID是否是一样,不一样就会升级为轻量级锁。(非官方,非权威,个人理解)

尝试在第二个线程前面增加一个线程:

new Thread(()->{
    try {
        Thread.sleep(20);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

结果很明显的从偏向锁变为轻量级锁了。

第一个线程。。。。
com.lry.thread.TestA object internals:
OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           05 78 c9 1f (00000101 01111000 11001001 00011111) (533297157)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4       int TestA.b                                   0
     16     1   boolean TestA.a                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

第二个线程。。。。
com.lry.thread.TestA object internals:
OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           60 f6 7c 20 (01100000 11110110 01111100 00100000) (545060448)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           05 c2 00 f8 (00000101 11000010 00000000 11111000) (-134168059)
     12     4       int TestA.b                                   0
     16     1   boolean TestA.a                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

批量重偏向

    当一个线程实例化多个对象并执行同步操作,后来另一个线程也对这些对象进行同步操作,进行了多次撤销偏向锁后,jvm会认为接下来的这些对象都需要批量重偏向,那么接下来的对象都是偏向锁;

    因为线程在同一个线程里执行相同的操作,并去对同一个对象进行操作,致使产生这样的结果。

这里可以这样设置:

当然我们可以通过-XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值

这里也存在上面偏向锁的问题,在第二个线程前增加一个线程去除线程复用,出现的结果才会是正常结果,那如果不加呢,出现的结果是概率性的,说不定某次你运行的时候,结果全部是偏向第一个线程,你会认为这就是重偏向,其实不对,你再多运行几次,就会发现,结果有很大的不同,jvm默认的批量重偏向阈值为20-40,那么正确结果是前19个,是轻量级锁,从第20个开始到40个结束,会是偏向锁。

  public static void main(String[] args) {
        List<TestA> list = new ArrayList<>();

        Thread t = new Thread(()->{
            for (int i = 0; i < 50; i++) {
                TestA a = new TestA();
                synchronized (a) {
                    list.add(a);
                    if (i == 49) {
                        System.out.println("第一个线程操作------");
                        System.out.println(ClassLayout.parseInstance(a).toPrintable());
                    }
                }
            }
        });

        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
// 增加一个线程 ,去占用第一个线程,避免线程复用
        new Thread(()->{
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        System.out.println("第二个线程操作------------------");
        Thread t2 = new Thread(()->{
            for (int i = 0; i < list.size(); i++) {
                synchronized (list.get(i)) {
                    if(i == 18 || i == 19){
                        System.out.println("第二个线程  计数:"+(i+1)+"-------------");
                        System.out.println(ClassLayout.parseInstance(list.get(i)).toPrintable());
                    }
                }
            }
        });
        t2.start();
    }

结果太长,就贴每个操作的第一次:

第一个线程操作------
com.lry.thread.TestA object internals:
OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           05 a8 25 20 (00000101 10101000 00100101 00100000) (539338757)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4       int TestA.b                                   0
     16     1   boolean TestA.a                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

第二个线程操作------------------
第二个线程  计数:19-------------
com.lry.thread.TestA object internals:
OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           18 f4 cb 20 (00011000 11110100 11001011 00100000) (550237208)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4       int TestA.b                                   0
     16     1   boolean TestA.a                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

第二个线程  计数:20-------------
com.lry.thread.TestA object internals:
OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           05 01 3b 20 (00000101 00000001 00111011 00100000) (540737797)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4       int TestA.b                                   0
     16     1   boolean TestA.a                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

批量撤销

根据上面批量重偏向得到的结果来看,线程A对多个对象实例化同步,线程B对这些对象同步操作,但是如果线程B只对这些对象操作20个,出现批量偏向,然后后面的不进行同步操作,这时,线程C过来了,继续对这些对象同步,当超过批量撤销的阈值后,就会将所有的对象转为轻量锁。

这里结果没得到证明,留下问题,之后再来研究。

2.轻量级锁什么时候发生的?

  • 单个线程

  • 多个线程交替执行  

  • 多个线程互斥执行

    当一个线程去拿一个资源的时候,发现得不到资源,然后会先自旋一段时间,然后再去拿,如果再拿不到,那么就会膨胀,具体的自旋时间需要看jvm源码。

    可以这样理解:一个线程去拿一个不属于自己线程的资源时,就会膨胀(不是很准确)

public static void main(String[] args) {
        TestA a = new TestA();
long start = System.currentTimeMillis();
        new Thread(()->{
            synchronized (a) {
                System.out.println(Thread.currentThread().getName()+" running ");
                System.out.println(ClassLayout.parseInstance(a).toPrintable());
                System.out.println("线程执行时间:"+(System.currentTimeMillis() - start ));
            }
        },"次线程").start();

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        synchronized (a) {
            System.out.println(Thread.currentThread().getName()+" running ");
            System.out.println(ClassLayout.parseInstance(a).toPrintable());
        }
    }

可见,交替执行的线程是轻量级锁

次线程 running
com.lry.thread.TestA object internals:
OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           80 f6 58 20 (10000000 11110110 01011000 00100000) (542701184)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4       int TestA.b                                   0
     16     1   boolean TestA.a                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

线程执行时间:3719
main running
com.lry.thread.TestA object internals:
OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           88 f1 9b 02 (10001000 11110001 10011011 00000010) (43774344)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4       int TestA.b                                   0
     16     1   boolean TestA.a                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

如果在第一个线程修改如下,加锁后增加睡眠时间,分别设置2秒和5秒,

 new Thread(()->{
            synchronized (a) {
                System.out.println(Thread.currentThread().getName()+" running ");
                System.out.println(ClassLayout.parseInstance(a).toPrintable());
                try {
// 分别设置2秒和5秒
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程执行时间:"+(System.currentTimeMillis() - start ));
            }
        },"次线程").start();

结果会发现:

设置两秒的时候,最后的对象是轻量级锁,因为避开了资源争夺,设置5秒的时候,第一个线程持有,并没有释放,导致第二个线程一直在申请锁,最后锁膨胀为重量级锁。

总结

偏向锁产生情况:

  1. 启动一段时间后;
  2. 单线程持有;
  3. 重偏向;
  4. 批量重偏向;

轻量级锁产生情况:

  1. 一开始启动那会是轻量级锁
  2. 互斥执行:线程A持有,线程B也想持有,但A持有中,B先自旋一段时间(这个时间jvm内部的,具体我不知道),拿到锁后,因为锁原本偏向A线程,这时被B拿走,就膨胀为轻量级锁,长时间拿不到就膨胀为重量级锁;
  3. 交替执行:线程A持有,线程B也想持有,但在A持有过程中,B没有去申请锁,在A释放后,B才去申请锁,这里存在重偏向问题,也不是真正的重偏向,及线程B会复用A的线程,在A B间再有一个线程可以避免复用;

重量级锁产生情况:

  1. 两个及两个以上竞争锁
  2. 调用wait方法后

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值