Thread疾风传·螺旋丸还是须佐能乎

Posted Danny_姜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Thread疾风传·螺旋丸还是须佐能乎相关的知识,希望对你有一定的参考价值。

在上一篇的 路漫漫其修远兮,吾将上下而求“锁” 中,我向大侠请教了几个线程相关的题目。可是很遗憾,始终没有寻得自己想要的结果。有不少小伙伴在看了文章之后,也对文中提到的问题表现出了一定的兴趣,所以就有了这一篇Thread疾风传--就当作是答疑解惑番外篇吧。

"锁"对象考察

第一题是通过对Synchronized的使用,考察其对Java中”锁“对象的理解。如图:

实际上,lock在SynchronizedTest类中是以非static成员变量的形式存在的,因此每创建一个Synchronized对象时,相应的,都会在堆内存中会创建新的lock对象。

虽然两个线程调用的是同样的startConditionLoop方法,但是synchronized(lock)中的lock是指向不同的内存地址,也就不是同一把锁,因此并不会存在互斥的作用。如下图:

但是,如果将lock改为static就不一样了,因为static在被创建在内存中的方法区,只会存在一个实例对象。因此即使是多个不同的线程,使用的锁都是指向内存中的同一地址,自然就有互斥作用了。如下图:

锁状态考察

第二题主要目的是想考察面试者对锁实现原理的理解,以及在不同并发级别下,锁相对应的状态。

其实主要是考察偏向锁、轻量级锁、重量级锁的理解

  • 当代码执行到图1处时,表示目前只有1个线程在持有锁,还没有发生锁竞争事件,所以此时锁的状态是 偏向锁 状态。

  • 当代码执行到图2处时,就表示有了第二个线程去申请锁对象,此时锁状态会被改写为 轻量级锁 状态。

  • 当代码执行到图3时,就存在多个线程去竞争锁对象,此时锁状态会被进一步升级为 重量级锁 状态。

注意:需要注意的是锁状态只有升级操作,并不会降级。也就是说即使threadA、threadB和threadC的代码都已经执行完毕,lock的状态依然是重量级锁,后续如果继续有新的线程申请lock锁,同样还是会导致从用户态到内核态的转换。

多线程编程考察

前面2道题虽然涉及到些微代码,但是还是偏理论知识多一些,因此最后出了一道编程题。题目不算难,就是按照顺序依次执行同一个实例的3个方法。这道题实际上是可以在LeetCode上搜到的,如图:

文中提到的大侠给出的做法是使用Thread.sleep()来实现,虽然这种解法,在大多数情况下打印出的结果都是正确的。但是,理论上这样实现并不能百分百保证first一定执行在second之前,second也并不一定执行在third之前。简单来说,就是因为CPU分配线程执行片段是随机的。

记得我在第一次做这道题的时候,下意识想到的是使用Lock的Condition来实现,具体来说就是通过两个不同的Condition分别控制代码的等待机制,如下所示:

基本思路就是在执行second时,先调用c2.await方法进行等待,然后之后threadA执行完之后,才通过signal来通知threadB继续执行。

同样threadC也是要等threadB执行完之后,才通过signal唤醒执行。按照这种实现也确实能够使first、second、third按序执行。

但是这种实现方式同样存在致命问题:当我多次运行程序时,发现偶尔会存在只打印first日志,然后程序就处于卡住停滞状态,但是红色终止按钮并没有显示灰色,这就表示程序没有执行完毕,很显然死锁了!

一番调试下来,发现问题原因:假如threadA在threadB running之前就已经执行完毕,也就是thread.signal已经被触发,而threadB再去执行到running中的代码调用c1.await,此时不会再有任何线程去唤醒此等待操作,造成死锁!

因此需要一个判断机制,如果在执行threadB时判断threadA已经执行过了,则不需要执行等待逻辑。修改后的代码如下:

  ReentrantLock lock = new ReentrantLock();
  Condition c1 = lock.newCondition();
  Condition c2 = lock.newCondition();
  private boolean firstPrinted = false;
  private boolean secondPrinted = false;

  private void testFooWithReentrantLock() 
      Thread t1 = new Thread(() -> 
          try 
              lock.lock();
              foo.first();
              firstPrinted = true;
              c1.signal();
           catch (Exception ignored)  finally lock.unlock();
      );

      Thread t2 = new Thread(() -> 
          try 
              lock.lock();
              c1.await();
              if (!firstPrinted) 
                  c1.await();
              
              foo.second();
              secondPrinted = true;
              c2.signal();
           catch (Exception ignored)  finally lock.unlock();
        );

      Thread t3 = new Thread(() -> 
          try 
              lock.lock();
              if (!secondPrinted) 
                  c2.await();
              
              c2.await();
              foo.third();
           catch (Exception ignored)  finally lock.unlock();
        );

      t3.start();
      t2.start();
      t1.start();
  

虽说是能够实现效果了,但是对于上述实现方式,总感觉有点偏麻烦。所以事后也一直思考有么有更加简洁的方式实现。果不其然,Java中有一个API就能实现上述Condition + 变量控制的效果,就是Semphore。使用Semphore进行重构之后,代码更加简洁,如下:

最终效果是一样的,但是代码却比之前更加简练且易懂。实际上,在JUC包有很多平时开发中都非常有用的接口或者集合。花一点时间深入研究,加以利用,相信对我们的工作效果相信会有很大帮助。

如果你喜欢本文

长按二维码关注

以上是关于Thread疾风传·螺旋丸还是须佐能乎的主要内容,如果未能解决你的问题,请参考以下文章

C语言编程学习当鸣人放了一个螺旋丸,我突然发觉这个事情不简单......

LeetCode54 螺旋矩阵,题目不重要,重要的是这个技巧

深度学习入行一年能干啥?菜鸟程序员开发系统识别火影手势,收获大把二次元粉丝

C语言题目 顺时针方向螺旋填充

初识docker-镜像

用java写了一个Http client,但向服务器post的时候传中文参数老是乱码,请大侠明示一下