大家,好久不见,从今天开始,我将给大家带来一个有意思的专题—
1.看电影也能学编程!
是的,就是告诉你如何一边看电影一边学java 多线程程序设计。
多线程是Java的核心内容之一,相关的概念和问题比较多,很多内容还非常难,这就导致很多人对这一块的内容总感觉不熟悉,不会用。老师带学生的时候,发现绝大部分对多线程这部分都学得非常一般。很多人在简历上写,Java基本功扎实,但是多线程能理解清楚的都不多。
其实呢,多线程学不好,不怪你,更不是你笨,而是教你的有问题,是你看的书有问题。这些材料很多故弄玄虚,简单的问题也介绍得很晦涩。在过去几年教学和代教过程中,我一直想找个形象例子将相关问题都集中梳理起来,做个教程,让更多粉丝们理解多线程的乐趣。
2025年初有个电影《蛟龙行动》上映,我本来很期待,但是看了之后感觉实在不怎么样,而且评分也不高。于是就将其姊妹电影,2018年的《红海行动》找出来看了一遍。这时候突然发现,里面很多场景都很符合多线程的概念,感觉特别适合讲解多线程里的内容。
如果将每个人视为一个线程的话 ,客观世界本来就是多线程的。比如说蛟龙队员们平时有自己事情,这就是线程在运行。战斗的时候需要与人交流就是线程通信。潜伏下来等待进攻的命令,这就是线程等待。在睡觉时被战斗警报叫醒,就可以理解为线程唤醒。等等。
所以呢,纵横老师准备制作一个系列,我们呢,一边看电影,一边学习Java编程。
今天是我们的第一期,我们先看一下电影的故事梗概,然后看一下哪些场景与多线程有关联,最后我们介绍一下如何用代码来演示电影里的场景。
《红海行动》以 2015 年也门撤侨真实事件为蓝本,讲述中国海军 “蛟龙突击队” 8 人小队在海外执行撤侨与反恐任务的生死历程,全程紧凑、层次清晰。
我们简单看一下 关键人物与任务关系表,如果没看过的话,可能不熟悉,没关系,我们大概知道就行。
| 角色 | 职责 | 核心贡献 | 命运 |
|---|
| 杨锐 | 队长 | 决策指挥、最终击毙头目 | 存活 |
| 顾顺 | 狙击手 | 击杀 “一只耳”、摧毁飞机 | 存活 |
| 李懂 | 观察员 | 弹道定位、协同狙击 | 存活 |
| 佟莉 | 机枪手 | 掩护队友、拆除黄饼 | 存活 |
| 张天德 | 机枪手 | 殿后掩护、牺牲 | 牺牲 |
| 庄羽 | 通讯兵 | 修复设备、牺牲 | 牺牲 |
| 徐宏 | 爆破手(副队长) | 开辟通道、排爆 | 存活(重伤) |
| 陆琛 | 医疗兵 | 多个 | |
| 夏楠 | 记者 | 提供黄饼情报、协助拆弹 | 存活 |
徐宏(杜江)、杨锐(张译)和张天德(王雨田):
观察手李董和狙击手顾顺(黄景瑜):
电影的整体脉络是反海盗→撤侨→公路遇到袭击→救人质→阻黄饼,
任务层层升级,从 “保护同胞” 扩展到 “维护地区安全”,体现中国军人的责任与担当,也为多线程、任务优先级等编程概念提供了绝佳的现实映射场景。
第一幕:序幕:索马里海域反海盗(任务 1:商船救援)
背景:中国商船 “广东号” 在索马里海域遭海盗劫持,部分船员遇害,剩余人质被困。
行动:蛟龙突击队(队长杨锐带队)潜入商船突袭,10 分钟内解救人质,狙击手罗星远程击中海盗船发动机,掩护队友行动。
转折:罗星追击海盗时被流弹击中脊柱神经,被迫退出,其狙击位由新队员顾顺接替。
新任务:返航途中,临沂号护卫舰接外交部紧急指令 —— 非洲伊维亚共和国政变,恐怖组织 “扎卡” 与叛军攻占首都,需立即前往撤侨。
第二幕:伊维亚共和国撤侨(任务 2:侨民撤离)
登陆部署:临沂号抵达伊维亚,蛟龙 8 人小队(杨锐、顾顺、李懂、佟莉、张天德、庄羽、徐宏、石头)登陆,负责护送华侨至港口登舰。
首次遇袭:护送最后一批侨民的车队遭扎卡组织伏击,队员依托建筑物固守,击退敌人但车辆受损。
支线出现(主要为后续工作铺垫):法籍华裔记者夏楠求助 —— 其助手邓梅(中国公民)与能源公司员工被扎卡绑架,且夏楠正追查扎卡手中的 “黄饼”(脏弹核原料)线索。
任务升级:扎卡发布威胁视频,以邓梅性命要挟交换政府官员家属,蛟龙小队决定在撤侨同时展开人质救援。
第三幕: 公路遇袭(临时紧急任务)
突击组车队行至工厂外围公路时,突然遭遇伏击,并且是遭遇敌方 “狙击 + 迫击炮” 组合打击,为此蛟龙小队的任务瞬间变成打退敌人,这一幕内容很丰富,包含大量的多线程内容,我们后面展开讲。
第四幕:解救人质
这段任务是深入交战区解救人质,这是整个电影的高潮,战斗比较复杂,代价也很大,两死两伤。
我们也是后面逐步展开。
第五慕:抢黄饼(脏弹核原料)
决策:虽伤亡惨重(仅剩杨锐、顾顺、李懂、佟莉 4 人能战),但为阻止核威胁,小队决定擅自行动(未获上级授权,只剩 40 分钟窗口期)。
这个电影怎么和高并发编程有联系呢?对于显示世界的话,本来就是多线程的,我们每个人都各自生活,是一个独立的线程。有时候我们需要和别人交流,这就像线程间通信。有时候任务很多,但是干活的牛马只有5个人 ,那就相当于一个线程池,等等,因此这两者就非常相似。那计算机为什么会有多线程呢?最大的原因是计算机的处理器的主要负责任务管理,而且速度很快,而具体需要干活的外部设备则慢很多。比如下载一个2小时的高清电影,处理器只需要几个指令,能瞬间完成。而真正要干活的网络、硬盘等,可能要辛辛苦苦很长时间。在下载电影的时候,很明显硬盘和带宽比较忙的,这个估计每个人都有同感,但是如果此时处理器也闲着就浪费了。所以呢,我们可以让处理器干其他事情,比如帮我们看电子书等等。所以为了让计算机的作用最大化,那我们就用到了多线程的设计,每个线程完成不同的任务。而处理器就负责管管这些线程的情况就行了。所以呢,殊途同归,现实世界和计算机的多线程就关联到一起了。那我们就可以从整体上看一下电影里的场景如何与多线程对应的:
| 电影场景 | 多线程概念 | 对应Java类/机制 |
|---|
| 队长指挥分工 | 线程池调度 | ThreadPoolExecutor |
| 情报共享更新 | 读写锁(安排重要任务时,其他人不说话的) | ReentrantReadWriteLock |
| 总攻前全员就位 | 屏障同步 | CyclicBarrier |
| 弹药补给链 | 生产者-消费者 | BlockingQueue |
| 夜视仪等争用 | 信号量(盾牌给我) | Semaphore |
| 等待登机完成 | 倒计时锁 | CountDownLatch |
| 呼叫空中支援 | 异步任务 | Future/CompletableFuture |
| 医疗包弹药箱僵持 | 死锁 | 锁顺序不一致 |
| 救援任务优先 | 线程优先级 | Thread.setPriority() |
我们再看一个更具体的例子:
屏障同步(CyclicBarrier),这是很多同学都感到陌生的概念,但在电影里往往是最紧张、最刺激的时候,就是我方全队都做好准备,准备收割敌人的时候。
在红海行动中有两个典型的场景,一个是商船上发起进攻的时候,另一个是解救人质发起进攻的时候:
狙击组需要就位
爆破组需要安装炸药
突击组需要到达攻击位置
全部就位后才能同时发起攻击
这其实就是屏障同步要干的事情。
再比如,读写锁场景,关于读写的问题,在Java里大量场景都用到,例如多线程有读写锁、Mysql有读写分离的设计、redis也有多写等等。显示中大部分场景都是读的多,而写的少。而且读的时候可以多个一起读,而写的时候就只能一个写,否则就冲突了。电影里,也有典型的场景就是队长在下达紧急命令的时候,其他人都是认真听 ,不能插话的,等等。
电影里,每个场景都是很复杂的 ,我们该如何模拟呢?当然是要用简单的代码示意一下,重点在于结合电影让我们更清晰地理解多线程里的典型思想和原理,而不是要完整模拟出来。
今天我们先来看个热身问题,如何将每个队员抽象为一个线程。
2 将队员抽象为线程的方法
学过java编程的同学应该知道,创建线程的方式主要有三种:实现Runnable接口、继承Thread类或者实现Callable接口,我们一个个来看一下。当然还能使用线程池来创建,这个后面单独讨论。
【1】 实现Runnable接口创建线程
Runnable是一个线程的接口,使用方法如下:
public class RunnableTest implements Runnable { @Override public void run() { System.out.println("run "); } public static void main(String[] args) { Thread thread = new Thread(new RunnableTest()); thread.start(); System.out.println("main..."); }}注意启动线程要通过thead.start()来启动的,线程启动之后,会执行run()方法。为什么如此我们后面再讲解。
【2】 继承Thread类创建线程
Thread是一个类,可以直接继承该类来创建线程,使用方法如下:
public class ThreadTest extends Thread { @Override public void run() { System.out.println(" sun thread hello "); } public static void main(String[] args) { ThreadTest threadTest = new ThreadTest(); threadTest.start(); System.out.println("main thread"); }}Runnable和Thread的区别在于,前者是接口而后者是类,因此如果当前要实现线程的类已经继承了另外一个类,就无法再继承Thread类了,只能通过Runnable接口来完成。
【3】实现Callable接口并创建带返回值的线程
在有些场景中,我们需要让一个异步执行的任务在执行完成后返回一个结果,而前面使用的线程中,run()方法是一个void修饰的无返回值方法,而前面两种方式都无法满足我们的要求,因此Java提供了Callable和Future组合创建返回值的线程。使用方法如下:
public class CallableExample implements Callable<String> { @Override public String call() throws Exception { return "执行call"; } public static void main(String[] args) throws ExecutionException, InterruptedException { CallableExample callableExample = new CallableExample(); FutureTask<String> futureTask = new FutureTask<>(callableExample); Thread thread = new Thread(futureTask); thread.start(); System.out.println("result" + futureTask.get()); }}可以看到Callable和Future组合还是略有复杂的,需要作为一个参数传递给thread。
这里为什么这么复杂呢?主要是因为Runnable的局限性。
Runnable设计的很早,其基础定义中没有返回结果,也不能处理异常:
public interface Runnable { /** * Runs this operation. */ void run();}既然没有,那我们加一个不就行了吗,但是呢Java还要考虑就代码的兼容等等,就不想调整Runnable,所以就另外建了个新接口和Runnable联合使用:
public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception;}这是最重要的原因,当然为了提供更多更丰富的方法,所以新接口也变得复杂很多。这种方式在很多工程的源码中会看到,我们后面会单独看。
理解了创建线程的基本方法之后,接下来,我们先定义一个基本的士兵线程:
先定义个士兵类,包含士兵的信息和执行任务。
public class Soldier implements Runnable { private String name; private String work = "waiting"; public Soldier(String name) { this.name = name; } public void setWork(String work) { this.work = work; } public void run() { System.out.println("我是 " + name + ", 我现在的任务是 :" + work);// 每个人不能不能地干一件事,模拟持续一段时间之后子线程就结构了 work(); System.out.println(name + work + "完成"); } /** * 模拟持续一段时间之后子线程就结构了 */ private void work() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }}接下来,我们就通过代码来逐步实现上述过程。
比如说,在公路遇袭阶段时,队长喊有没有事快点回话,之后队员们纷纷回复,那我们就可以这么写,主线程是队长,队员是子线程:
public class RegisterThread { private static Thread 石头Thread, 佟莉Thread, 顾顺Thread, 庄羽Thread, 徐宏Thread, 李懂Thread, 陆琛Thread; public static void main(String[] args) { //step 1: 了解战场情况,初步防守 Soldier 石头 = new Soldier("石头"); 石头.setWork("我没事"); 石头Thread = new Thread(石头); 石头Thread.start(); Soldier 佟莉 = new Soldier("佟莉"); 佟莉.setWork("我没事"); 佟莉Thread = new Thread(佟莉); 佟莉Thread.start(); Soldier 庄羽 = new Soldier("庄羽"); 庄羽.setWork("我没事"); 庄羽Thread = new Thread(庄羽); 庄羽Thread.start(); Soldier 徐宏 = new Soldier("徐宏"); 徐宏.setWork("我没事"); 徐宏Thread = new Thread(徐宏); 徐宏Thread.start(); Soldier 陆琛 = new Soldier("陆琛"); 陆琛.setWork("我没事"); 陆琛Thread = new Thread(陆琛); 陆琛Thread.start(); Soldier 顾顺 = new Soldier("顾顺"); 顾顺.setWork("我没事"); 顾顺Thread = new Thread(顾顺); 顾顺Thread.start(); Soldier 李董 = new Soldier("李董"); 李董.setWork("我没事"); 李懂Thread = new Thread(李董); 李懂Thread.start(); }}之后队长开始安排任务,我们可以这么写:
public class FristTaskMainThread { private static Thread 石头Thread, 佟莉Thread, 顾顺Thread, 庄羽Thread, 徐宏Thread, 李懂Thread, 陆琛Thread; public static void main(String[] args) { //step 1: 了解战场情况,初步防守 Soldier 石头 = new Soldier("石头"); 石头.setWork("持续防守后方"); 石头Thread = new Thread(石头); 石头Thread.start(); Soldier 佟莉 = new Soldier("佟莉"); 佟莉.setWork("持续侧翼攻击"); 佟莉Thread = new Thread(佟莉); 佟莉Thread.start(); Soldier 庄羽 = new Soldier("庄羽"); 庄羽.setWork("找武器"); 庄羽Thread = new Thread(庄羽); 庄羽Thread.start(); Soldier 徐宏 = new Soldier("徐宏"); 徐宏.setWork("去救人质"); 徐宏Thread = new Thread(徐宏); 徐宏Thread.start(); Soldier 陆琛 = new Soldier("陆琛"); 陆琛.setWork("定位敌人阵地"); 陆琛Thread = new Thread(陆琛); 陆琛Thread.start(); Soldier 顾顺 = new Soldier("顾顺"); 顾顺.setWork("不具备狙击条件,原地战斗"); 顾顺Thread = new Thread(顾顺); 顾顺Thread.start(); Soldier 李董 = new Soldier("李董"); 李董.setWork("不具备狙击条件,原地战斗"); 李懂Thread = new Thread(徐宏); 李懂Thread.start(); }}整个电影里有大量有意思的场景,我们接下来就大致按照电影的顺序来制作大约10期的内容。主要看大家的反馈,如果效果好,我们就继续做更多。
不过呢,我们分享的顺序大致是按照电影的时间顺序来推进,但是会兼顾每个点的难易程度、重要性等来调整。而如果不是很明显,而又比较重要的内容,我们也会参考其他的电影,比如《战狼2》、《东成西就》等来分享。
我们后面介绍每个技能点的时候都分三部分:与电影结合介绍技术原来这么简单;基本代码模拟电影场景;该技能在开源框架中的典型应用,让你深入浅出、张弛有度、理论与实践完美结合。
今天我们就分享到这里,如果您觉得我做的内容还凑合,欢迎点赞、关注和转发,阅读过千,三天内我们制作出下一期。谢谢大家!