并发王者课-黄金3:雨露均沾-不要让你的线程在竞争中被“饿死”

Posted JAVA炭烧

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发王者课-黄金3:雨露均沾-不要让你的线程在竞争中被“饿死”相关的知识,希望对你有一定的参考价值。

在并发编程中,除了死锁之外,还有一些同样重要的线程活跃性问题值得关注。它们的知名度不高,但破坏性极强,本文将介绍的正是其中的线程饥饿和活锁问题。

一、饥饿的产生

所谓线程 饥饿(Starvation) 指的是在多线程的资源竞争中,存在贪婪的线程一直锁定资源不释放,其他的线程则始终处于等待状态,然而这个等待是没有结果的,它们会被活活地饿死。

独占者的贪婪是饥饿产生的原因之一,概括来说,饥饿一般由下面三种原因导致:
(1)线程被无限阻塞
当获得锁的线程需要执行无限时间长的操作时(比如IO或者无限循环),那么后面的线程将会被无限阻塞,导致被饿死。
(2) 线程优先级降低没有获得CPU时间
当多个竞争的线程被设置优先级之后,优先级越高,线程被给予的CPU时间越多。在某些极端情况下,低优先级的线程可能永远无法被授予充足的CPU时间,从而导致被饿死。
(3) 线程永远在等待资源
在青铜系列文章中,我们说过notify在发送通知时,是无法唤醒指定线程的。当多个线程都处于wait时,那么部分线程可能始终无法被通知到,以至于挨饿。

二、饥饿与公平

为了直观体验线程的饥饿,我们创建了下面的代码。

创建哪吒、兰陵王等四个英雄玩家,他们以竞争的方式打野,杀死野怪可以获得经济收益。

public class StarvationExample {

  public static void main(String[] args) {
    final WildMonster wildMonster = new WildMonster();

    String[] players = {
      "哪吒",
      "兰陵王",
      "铠",
      "典韦"
    };
    for (String player: players) {
      Thread playerThread = new Thread(new Runnable() {
        public void run() {
          wildMonster.killWildMonster();
        }
      });
      playerThread.setName(player);
      playerThread.start();
    }
  }
}
 public class WildMonster {
   public synchronized void killWildMonster() {
     while (true) {
       String playerName = Thread.currentThread().getName();
       System.out.println(playerName + "斩获野怪!");
       try {
         Thread.sleep(500);
       } catch (InterruptedException e) {
         System.out.println("打野中断");
       }
     }
   }
 }

运行结果如下:

哪吒斩获野怪!
哪吒斩获野怪!
哪吒斩获野怪!
哪吒斩获野怪!
哪吒斩获野怪!
哪吒斩获野怪!
哪吒斩获野怪!
哪吒斩获野怪!
哪吒斩获野怪!
哪吒斩获野怪!
哪吒斩获野怪!

Process finished with exit code 130 (interrupted by signal 2: SIGINT)

从结果中可以看到,在几个线程的运行中,始终只有哪吒可以斩获野怪,其他英雄束手无策等着被饿死。为什么会发生这样的事?

仔细看WildMonster类中的代码,问题出在killWildMonster同步方法中。一旦某个英雄进入该方法后,将一直持有对象锁,其他线程被阻塞而无法再进入

当然,解决的方法也很简单,只要打破独占即可。比如,我们在下面的代码中把Thread.sleep改成wait,那么问题将迎刃而解。

 public static class WildMonster {
   public synchronized void killWildMonster() {
     while (true) {
       String playerName = Thread.currentThread().getName();
       System.out.println(playerName + "斩获野怪!");
       try {
         wait(500);
       } catch (InterruptedException e) {
         System.out.println("打野中断");
       }
     }
   }
 }

运行结果如下:

哪吒斩获野怪!
铠斩获野怪!
兰陵王斩获野怪!
典韦斩获野怪!
兰陵王斩获野怪!
典韦斩获野怪!

Process finished with exit code 130 (interrupted by signal 2: SIGINT)

从结果中可以看到,四个英雄都获得了打野的机会,在一定程度上实现了公平。(备注:wait会释放锁,但sleep不会,对此不理解的可以查看青铜系列文章。)

如何让线程之间公平竞争,是线程问题中的重要话题。虽然我们无法保证百分百的公平,但我们仍然要通过设计一定的数据结构和使用相应的工具类来增加线程之间的公平性。

关于线程之间的公平性,在本文中重要的是理解它的存在和重要性,关于如何优雅地解决,我们会在后续的文章中介绍相关的并发工具类。

三、活锁的麻烦

相对于死锁,你可能对活锁没有那么熟悉。然而,活锁所造成的负面影响并不亚于死锁。在结果上,活锁和死锁都是灾难性的,都将会造成应用程序无法提供正常的服务能力。

所谓活锁(LiveLock),指的是两个线程都忙于响应对方的请求,但却不干自己的事。它们不断地重复特定的代码,却一事无成。

不同于死锁,活锁并不会造成线程进入阻塞状态,但它们会原地打转,所以在影响上和死锁相似,程序会进入无线死循环,无法继续进行。

如果你无法直观理解活锁是什么,相信你在走路时一定遇到过下面这种情况。两人相向而行,出于礼貌两人互相让行,让来让去,结果两人仍然无法通行。活锁,也是这个意思。

小结

以上就是关于线程饥饿与活锁的全部内容。在本文中,我们介绍了线程产生饥饿的原因。对待线程饥饿,没有百分百的方案,但可以尽可能地实现公平竞争。我们没有在本文列举线程公平性的一些工具类,因为我认为对问题的理解要比解决方案更重要。如果没有对问题的理解,方案在落地时也会出现知其然而不知其所以然的情况。另外,虽然活锁并不像死锁那样知名度,但是对活锁的恰当理解仍然非常必要,它是并发知识体系中的一部分。

最后

最近我整理了整套《JAVA核心知识点总结》,说实话 ,作为一名Java程序员,不论你需不需要面试都应该好好看下这份资料。拿到手总是不亏的~我的不少粉丝也因此拿到腾讯字节快手等公司的Offer

Java进阶群

好了,以上就是本文的全部内容了,如果觉得有收获,记得三连,我们下期再见。

以上是关于并发王者课-黄金3:雨露均沾-不要让你的线程在竞争中被“饿死”的主要内容,如果未能解决你的问题,请参考以下文章

如何做到"雨露均沾"?假如皇帝也懂负载均衡算法...

王者并发课-铂金4:令行禁止-为何说信号量是线程间的同步利器

Python并发之协程

并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁

并发王者课-青铜6:借花献佛-如何格式化Java内存工具JOL输出

不要质疑你的付出,这些都会是一种累积一种沉淀,它们会默默铺路,只为让你成为更优秀的人