从「哪吒抢笔」学习多线程编程

点击上方 蓝字 关注我们

下面开始今天的学习~

最近刚看完大热的《哪吒之魔童降世》,唯美的中国风、催泪的剧情、打破固有的成见、解开命运的枷锁……饺子导演带领团队十一年磨一剑的幕后制作故事让人深受感动,实乃国漫良心之作。

在生活中,作为程序员要保持将身边发生的事情代入计算机思考的习惯。 其中,在江山社稷图中的一段剧情让人印象深刻:哪吒、太乙真人、敖丙、申公豹四个人为了打开江山社稷图,共同争夺同一支毛笔,由此引发大混乱。

这熟悉的资源争夺, 不正像是多线程并发执行引发的竞争状态吗多线程编程早已不是一个新鲜概念,在现代计算机的硬件支持下,开启多个线程分段执行耗时任务已成为一种常见的性能优化手段,并且使用多线程还可以充分利用 CPU 的资源。

进程、线程与并发

要理解多线程,需要先搞懂几个概念:

  • 进程 :每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。

  • 线程 :线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。线程是程序中一个单一的顺序控制流程。

  • 多线程 :在单个程序中同时运行多个线程完成不同的工作,称为多线程。

  • 并发 :在操作系统中,并发是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

线程和进程的区别在于, 子进程和父进程有不同的代码和数据空间而多个线程则共享数据空间 ,每个线程有自己的执行堆栈和程序计数器为其执行上下文。 多线程主要是为了节约 CPU 时间 ,线程的运行中需要使用计算机的内存资源和 CPU。

多线程应用

下面我们就使用多线程来模拟一下四人抢笔的这段剧情。

先新建一个江山社稷图类:


object Painting {
var pen = true
}

哪吒、太乙真人、敖丙、申公豹都有抢笔动作和打开社稷图动作,我们新建一个接口类来表示这两个动作:


interface ITask {

fun getPen():Boolean

fun openPainting()
}

接下来创建哪吒、太乙真人、敖丙、申公豹四个对象,实现此接口:

哪吒


class NeZha : ITask {


override fun getPen():Boolean {
return Painting.pen
}


override fun openPainting() {
// 花费一秒钟的时间使用钥匙打开江山社稷图
Thread.sleep(1000)
println(“哪吒打开了江山社稷图”)
Painting.pen = false
}
}

可以看到,我们在代码中通过 getPen() 方法获取江山社稷图中的 pen 字段,在 openPainting() 方法中,花费一秒钟的时间打开江山社稷图,然后将 pen 字段置为 false ,表示笔已被使用。太乙真人、敖丙和申公豹类的代码是类似的。

太乙真人:


class TaiYiZhenRen : ITask {

override fun getPen():Boolean {
return Painting.pen
}


override fun openPainting() {
Thread.sleep(1000)
println(“太乙真人打开了江山社稷图”)
Painting.pen = false
}
}

敖丙:


class AoBing : ITask {


override fun getPen():Boolean {
return Painting.pen
}


override fun openPainting() {
Thread.sleep(1000)
println(“敖丙打开了江山社稷图”)
Painting.pen = false
}
}

申公豹:


class ShenGongBao : ITask {

override fun getPen():Boolean {
return Painting.pen
}


override fun openPainting() {
Thread.sleep(1000)
println(“申公豹打开了江山社稷图”)
Painting.pen = false
}
}

然后我们新建一个 Fight 类,模拟四人的抢笔场景,程序流程如下:

  • 创建哪吒、太乙真热、敖丙、申公豹实例对象;

  • 为每个对象开启一个子线程,在子线程内完成抢笔、打开江山社稷图操作;

  • 等待两秒,保证所有线程运行完成,观察结果。


class Fight {
@Test
fun start() {
val neZha = NeZha()
val taiYiZhenRen = TaiYiZhenRen()
val aoBing = AoBing()
val shenGongBao = ShenGongBao()
Thread {
if (neZha.getPen()) {
neZha.openPainting()
} else {
println(“哪吒没有抢到笔。”)
}
}.start()
Thread {
if (taiYiZhenRen.getPen()) {
taiYiZhenRen.openPainting()
} else {
println(“太乙真人没有抢到笔。”)
}
}.start()
Thread {
if (aoBing.getPen()) {
aoBing.openPainting()
} else {
println(“敖丙没有抢到笔。”)
}
}.start()
Thread {
if (shenGongBao.getPen()) {
shenGongBao.openPainting()
} else {
println(“申公豹没有抢到笔。”)
}
}.start()


Thread.sleep(2000)
}
}

大功告成!运行程序,控制台显示如下:


哪吒打开了江山社稷图
申公豹打开了江山社稷图
太乙真人打开了江山社稷图
敖丙打开了江山社稷图

E x cuse me ? 明 明每个人都在打开社稷图后将 pen 置为了 false ,为什么四个人都拿到了笔打 开了江山社稷图?

这就是多线程编程中常出现的并发问题,当多个线程访问同一个对象时,会引发竞争状态,导致结果不正确

要解决此问题,需要使用 synchronized 关键字。synchronize 直译为”同步化”,在《Head First Java》一书中,对 synchronize 关键字是这样介绍的:

每个 Java 对象都有锁,每个锁只有一把钥匙,但大多数情况下对象都没有上锁。但如果对象有同步化的方法,则线程只能在取得钥匙的情况下进入线程。也就是说只能在没有其他线程已经进入的情况下才能进入。

本例中,synchronize 的作用是当哪吒、太乙真人、敖丙、申公豹中的任何一个人抢到笔后,将笔”锁住”,直至使用完后才释放。将程序修改如下:


class Fight {
@Test
fun start() {
val neZha = NeZha()
val taiYiZhenRen = TaiYiZhenRen()
val aoBing = AoBing()
val shenGongBao = ShenGongBao()
Thread {
synchronized(Painting.pen) {
if (neZha.getPen()) {
neZha.openPainting()
} else {
println(“哪吒没有抢到笔。”)
}
}
}.start()
Thread {
synchronized(Painting.pen) {
if (taiYiZhenRen.getPen()) {
taiYiZhenRen.openPainting()
} else {
println(“太乙真人没有抢到笔。”)
}
}
}.start()
Thread {
synchronized(Painting.pen) {
if (aoBing.getPen()) {
aoBing.openPainting()
} else {
println(“敖丙没有抢到笔。”)
}
}
}.start()
Thread {
synchronized(Painting.pen) {
if (shenGongBao.getPen()) {
shenGongBao.openPainting()
} else {
println(“申公豹没有抢到笔。”)
}
}
}.start()


Thread.sleep(2000)
}
}

然后,运行程序,输出如下:


哪吒打开了江山社稷图
申公豹没有抢到笔。
敖丙没有抢到笔。

太乙真人没有抢到笔。

这样就实现了多线程的同步化, 以上就是一个多线程编程的简单应用场景。

多线程常见考点

近年来随着中国程序员水平的整体提高,多线程问题也成为了大厂面试官的新宠。实际工作中遇到的多线程问题往往复杂得多,常见考察点有:

  • 线程的生命周期: 包括就绪状态、运行状态、阻塞状态等

  • 线程池的原理、使用场景、常见配置

  • 线程的调度和优先级

  • 两个或两个以上线程互相阻塞引发的死锁问题等

考点虽多,但 个中原理其实并不复杂,细心的小伙伴可能已经发现了,力扣题库中最新上线了多线程编程题,帮助你把多线程问题抽丝剥茧、各个击破。在题库页面 多线程编程题 就可以看到:

力扣的多线程题库还在不断更新中,想要练手多线程题目的同学们赶紧先练习起来吧~

本文作者:Alpinist Wang

编辑&版式:霍霍

声明:本文归 “力扣” 版权所有,如需转载请联系。

文中部分图片来源于网络,为非商业用途使用,如有侵权联系删除。

推荐阅读