前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >多线程五 锁的膨胀过程

多线程五 锁的膨胀过程

作者头像
用针戳左手中指指头
发布2021-01-29 11:02:15
2840
发布2021-01-29 11:02:15
举报
文章被收录于专栏:学习计划

上一篇中,涉及到了锁升级的过程,也对其锁的升级有了一个大概的了解:单线程持有,在jvm延迟偏向的时间内是轻量级锁,之后为偏向锁,出现多个线程交替执行,对同一资源加锁会升级为轻量级锁,多个线程竞争拿不到锁会升级为重量级锁。在上一篇的基础上再进一步的了解锁升级的过程。

1.偏向锁发生情况?

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

还有一种情况:偏向锁:

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

代码语言:javascript
复制
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是不是轻量级锁?

代码语言:javascript
复制
第一个线程。。。。
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是否是一样,不一样就会升级为轻量级锁。(非官方,非权威,个人理解)

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

代码语言:javascript
复制
new Thread(()->{
    try {
        Thread.sleep(20);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

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

代码语言:javascript
复制
第一个线程。。。。
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个结束,会是偏向锁。

代码语言:javascript
复制
  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();
    }

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

代码语言:javascript
复制
第一个线程操作------
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源码。 可以这样理解:一个线程去拿一个不属于自己线程的资源时,就会膨胀(不是很准确)
代码语言:javascript
复制
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());
        }
    }

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

代码语言:javascript
复制
次线程 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秒,

代码语言:javascript
复制
 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方法后
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/04/07 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.偏向锁发生情况?
  • 2.轻量级锁什么时候发生的?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档