写点什么

并发王者课 - 青铜 8:分工协作 - 从本质认知线程的状态和动作方法

用户头像
秦二爷
关注
发布于: 2021 年 05 月 31 日

欢迎来到《并发王者课》,本文是该系列文章中的第 8 篇


在本篇文章中,我将从多线程的本质出发,为你介绍线程相关的状态和它们的变迁方式,并帮助你掌握这块知识点

一、多线程的本质是分工协作

如果你是王者的玩家,那么你一定知道王者中的众多英雄分为主要分为几类,比如法师战士坦克辅助等等。一些玩家对这些分类可能并不了解,甚至会觉得,干嘛要搞得这么复杂,干不完了嘛。这...当然不可以


抱此想法的如果不是青铜玩家,想必就是战场上的那些个人英雄主义玩家,在他们眼里没有团队。然而,只有王者知道,比赛胜利的关键,在于团队的分工协作各自为战必将一团乱麻、溃不成军,正所谓单丝不成线,独木难成林


分工协作无处不在,峡谷中需要分工协作,现实中我们的工作更是社会化分工的结果,因为社会的本质就是分工协作


而我要告诉你的是,在并发编程里,多线程的本质也是分工协作,每个线程恰似一个英雄,有着自己的职责、状态和技能(动作方法)。所谓线程的状态、方法实现不过都是为了完成线程间的分工协作。换句话说,线程状态的存在不是目的,而是实现分工协作的方式。所以,理解线程的线程状态和驱动方法,首先要理解它们为什么而存在


二、从协作认知线程的状态

线程的状态是线程在协作过程中的瞬时特征。根据协作的需要,线程总共有六种状态,分别是 NEWRUNNABLEWAITINGTIMED_WAITINGBLOCKED TERMINATED 等。比如,我们创建一个英雄哪吒的线程neZhaPlayer


Thread neZhaPlayer = new Thread(new NeZhaRunnable());
复制代码


那么,线程创建之后,接下来它将在下图所示的六种状态中变迁。刚创建的线程处于 NEW 的状态,而如果我们调用neZhaPlayer.start(),那它将会进入 RUNNABLE 状态。



六种不同状态的含义是这样的:


  • NEW:线程新建但尚未启动时所处的状态,比如上面的neZhaPlayer

  • RUNNABLE:在 Java 虚拟机中执行的线程所处状态。需要注意的是,虽然线程当前正在被执行,但可能正在等待其他线程释放资源;

  • WAITING无限期等待另一个线程执行特定操作来解除自己的等待状态;

  • TIMED_WAITING限时等待另一个线程执行或自我解除等待状态;

  • BLOCKED被阻塞等待其他线程释放 Monitor Lock;

  • TERMINATED:线程执行结束。


在任意特定时刻,一个线程都只能处于上述六种状态中的一种。需要你注意的是 RUNNABLE 这个状态,它有些特殊。确切地说,它包含 READY RUNNING 两个细分状态,下一章节的图示中有明确标示。


另外,前面我们已经介绍过 Thread 类,对于线程各状态的表述,你可以直接阅读 JDK 中的Thread.State枚举,并可以通过Thread.getState()查看当前线程的瞬时状态。

三、从线程状态变迁看背后的方法驱动

和人类的交流类似,在多线程的协作时,它们也需要交流。所以,线程状态的变迁需就要不同的方法来实现交流,比如刚创建的线程需要通过调用start()将线程状态由 NEW 变迁为 RUNNABLE


下图所展示的正是线程间的状态变迁以及相关的驱动方法,你可以先大概浏览一遍,随后再结合下文的各关键方法的表述深入理解。



需要注意的是,本文不会详细介绍线程状态相关的所有方法,这既不现实也毫无必要。上面这幅宝藏图示是理解本文所述知识的核心,下面所介绍的几个主要方法也并非为了你记忆,而是为了让你更好理解上面这幅图


在你理解了这幅宝图之后,你便可以完全自行去了解其他更多的方法。

1. start:对战开始,敌军还有 5 秒到达战场

public class NeZhaRunnable implements Runnable {    public void run() {        System.out.println("我是哪吒,我去上路");    }}
Thread neZhaPlayer = new Thread(new NeZhaRunnable());neZhaPlayer.start();
复制代码


start()方法主要将完成线程状态从 NEW RUNNABLE 的变迁,这里有两个点:


  • 创建新的线程;

  • 由新的线程执行其中的run()方法。


需要注意的是,你不可以重复调用start()方法,否则会抛出IllegalThreadStateException异常。

2. wait 和 notify:我在等你,好了请告诉我

哪吒每次在使用完大招后,都需要经历几十秒的冷却时间才可以再次使用,接下来我们通过代码片段来模拟这个过程。


我们先定义一个Player类,这个类中包含了fight()refreshSkills()两个方法,分别用于进攻和技能刷新,代码片段如下。


public class Player {    public void fight() {        System.out.println("大招未就绪,冷却中...");        synchronized (this) {            try {                this.wait();                System.out.println("大招已就绪,发起进攻!");            } catch (InterruptedException e) {                System.out.println("大招冷却被中断!");            }        }    }    public void refreshSkills() {        System.out.println("技能刷新中...");        synchronized (this) {            this.notifyAll();            System.out.println("技能已刷新!");        }    }}
复制代码


随后,我们写一段main()方法使用刚才创建的Player。注意,这里我们创建了两个线程分别调用Player中的不同方法


public static void main(String[] args) throws InterruptedException {        final Player neZha = new Player();        Thread neZhaFightThread = new Thread() {            public void run() {                neZha.fight();            }        };        Thread skillRefreshThread = new Thread() {            public void run() {                neZha.refreshSkills();            }        };        neZhaFightThread.start();        skillRefreshThread.start();    }
复制代码


代码运行结果如下:


大招未就绪,冷却中...技能刷新中...技能已刷新!大招已就绪,发起进攻!
Process finished with exit code 0
复制代码


从运行的结果看,符合预期。相信你已经看到了,在上面的代码中我们使用了wait()notify()两个函数。这两个线程是如何协作的呢?往下看。


首先,neZhaAttachThread 调用了neZha.fight()这个方法。可是,当哪吒想发起进攻的时候,竟然大招还没有冷却!于是,这个线程不得不通过wait()方法进入等待队列


紧接着,skillRefreshThread 调用了neZha.refreshSkills()这个方法。并且,在执行结束后又调用了notify()方法。有趣的事情发生了,前面处于等待队列中的 neZhaAttachThread 竟然又“复活”了,并且大喊了一声:大招已经就绪,发起进攻!


这是怎么回事?理解这块逻辑,你需要了解以下几个知识点:


  • wait():看到wait()时,你可以简单粗暴地认为每个对象都有一个类似于休息室的等待队列,而wait()正是把当前线程送进了等待队列并暂停继续执行;

  • notify():如果说wait()是把当前线程送进了等待队列,那么notify()则是从等待队列中取出线程。此外,和notify()具有相似功能的还有个notifyAll()。与notify()不同的是,notifyAll()会取出等待队列中的所有线程;


看到这,你是不是觉得wait()notify()简直是完美的一对?其实不然。真相不仅不完美,还很不靠谱!


wait()notify()在执行时都必须先获得锁,这也是你在代码中看到synchronized的原因。notify()在释放锁的时候,会从等待队列中取出线程,此时的线程必须获得锁之后才能继续运行。那么,问题来了。如果队列中有多个线程时,notify()能取出指定的线程吗?答案是不能!


换句话说,如果队列中有多个线程,你将无法预料后续的执行结果!notifyAll()虽然可以取出所有的线程,但最终也只能有一个线程能获得锁。


是不是有点懵?懵就对了。所以你看,wait()notify()是不是很不靠谱?因此,如果你需要在项目代码中使用它们,请务必要小心谨慎!


此外,如果你阅读过《Effective Java》,可以看到在这本书里作者 Josh Bloch 也是强烈建议不要随便使用这对组合。因为它们就像 Java 中的“汇编语言”,确实复杂且不容易控制,如果有相似的并发场景需要处理,可以考虑使用 Java 中的其他高级的并发工具。

3. interrupt:做完这一单,我就退隐江湖

在王者的游戏中,如果英雄血量没了,可以回城补血。回城大概需要 5 秒左右,如果在回城的过程中,突然被攻击或需要移位,那么回城就会中断。接下来,下面我们看看怎么模拟回城中的中断。


现在Player中定义backHome()方法用于回城。


 public void backHome() {        System.out.println("回城中...");        synchronized (this) {            try {                this.wait();                System.out.println("已回城");            } catch (InterruptedException e) {                System.out.println("回城被中断!");            }        }    }
复制代码


接下来启动新的线程调用backHome()回城补血。


public static void main(String[] args) throws InterruptedException {        final Player neZha = new Player();        Thread neZhaBackHomeThread = new Thread() {            public void run() {                neZha.backHome();            }        };        neZhaBackHomeThread.start();        neZhaBackHomeThread.interrupt();    }
复制代码


运行结果如下:


回城中...回城被中断!
Process finished with exit code 0
复制代码


可以看到,回城被中断了,因为我们调用了interrupt()方法!那么,在线程中的中断是怎么回事?往下看。


在 Thread 中,我们可以通过interrupt()中断线程。然而,如果你细心的话,还会发现 Thread 中除了interrupt()方法之外,竟然还有两个长相酷似的方法:interrupted()isInterrupted()。这就要小心了。


  • interrupt():将线程设置为中断状态;

  • interrupted():取消线程的中断状态;

  • isInterrupted():判断线程是否处于中断状态,而不会变更线程状态。


不得不说,interrupt()interrupted()这两个方法的命名实在糟糕,你在编码时可不要学习它,方法的名字应该清晰明了表达出其意图


那么,当我们调用interrupt()时,所调用对象的线程会立即抛出InterruptedException异常吗?其实不然,这里容易产生误解


interrupt()方法只是改变了线程中的中断状态而已,并不会直接抛出中断异常。中断异常的抛出必须是当前线程在执行wait()sleep()join()时才会抛出。换句话说,如果当前线程正在处理其他的逻辑运算,不会被中断,直到下次运行wait()sleep()join()

4. join:稍等,等我结束你再开始

在前面的示例中,哪吒发起进攻和技能刷新两个线程是同时开始的。然而,我们在前面已经说了wait()notify()并不靠谱,所以我们想在技能刷新结束后再执行后续动作。


public static void main(String[] args) throws InterruptedException {        final Player neZha = new Player();        Thread neZhaFightThread = new Thread() {            public void run() {                neZha.fight();            }        };        Thread skillRefreshThread = new Thread() {            public void run() {                neZha.refreshSkills();            }        };               skillRefreshThread.start();        skillRefreshThread.join(); //这里是重点        neZhaFightThread.start();    }
复制代码


主线程调用join()时,会阻塞当前线程继续运行,直到目标线程中的任务执行完毕。此外,在调用join()方法时,也可以设置超时时间。

小结

以上就是关于线程状态及变迁的全部内容。在本文中,我们介绍了多线程的本质是协作,而状态和动作方法是实现协作的方式。无论是面试还是其他的资料中,线程的状态方法都是重点。然而,我希望你明白了的是,对于本文知识点的掌握,不要从静态的角度死记硬背,而是要动静结合,从动态的方法认知静态的状态


正文到此结束,恭喜你又上了一颗星✨


夫子的试炼


在本文中,我们并没有提到yield()Thread.sleep()Thread.current()等方法。不过,如果你感兴趣的话,不妨检索资料:


  • 了解yield()并对比它和join()的不同;

  • 了解wait()并对比它和Thread.sleep()的不同;

  • 了解Thread.current()的主要用法和它的实现。


延伸阅读



关于作者


关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不做标题党。


如果本文对你有帮助,欢迎点赞关注监督,我们一起从青铜到王者

发布于: 2021 年 05 月 31 日阅读数: 5
用户头像

秦二爷

关注

微信公众号:【庸人技术笑谈】 2018.05.13 加入

记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不兜售课程。

评论

发布
暂无评论
并发王者课-青铜8:分工协作-从本质认知线程的状态和动作方法