点击上方蓝字关注我们
下面开始今天的学习~
月圆之夜,洛洛山。
洛洛山坐落于多线程王国的一个偏远小镇,山上绿树环绕,风光灵秀。踏着蜿蜒的小路循迹而上,花香、鸟啼、青柳、虫鸣,共同演奏者一曲天地之歌,大自然的鬼斧神工和洛洛山大当家——张麻子的笑声一样豪放不羁。张麻子占领洛洛山十年有余,寨子创立之初,他便定下规矩:“三不劫”——老少不劫、农民不劫、妇人不劫。只对路过的富商、官宦等有钱有势的人家下手,收取过路费用。在他多年用心经营之下,寨子已小有规模。寨中壮丁上千许,库内酒钱上万贯。张麻子作为大当家,正是意气风发之时,问苍茫大地,我主沉浮!终日饮酒作乐,好不快活。
人到无求品自高,张麻子在洛洛山势力足够大时,便很少再管束寨子中的事,渐渐地任其自由生长。洛洛山的手下们长期过着优渥的生活,不免沾染上些纨绔之气。寨子从最开始的劫富济贫发展到后来,反倒成了一方恶霸,手下的人不规矩,老少劫、农民劫、妇人也劫,镇上人民苦不堪言。张麻子本人却越来越不问世事,心怀通达,见到谁都是满面笑容,让人如沐春风之中。
不过,在你眼里张麻子的笑就不是那么让人欢喜了。男儿何不带吴钩?作为一名有志青年,你也曾纵马横刀,决心占山为王。然而终敌不过洛洛山传承已久,底蕴深厚。最终你只能在次一等的皮皮山安营扎寨,皮皮山地势险峻,易守难攻。物产又极其匮乏,张麻子不屑于为皮皮山上的蝇头小利大动干戈,这些年来,两座山头倒也相安无事。
然而山珍海味摆在眼前,谁又能将就得了咸菜萝卜。你早已在心中暗暗发誓,势必占领洛洛山头!正值十五月圆夜,谋事月黑风高时。你率领手下八百余人悄然围攻洛洛山,之所以选择十五是因为你知道月儿越明,星星越稀少,若再遇上乌云遮蔽月色,端的是伸手不见五指,正是动手的好时机。你决定趁着今夜云蒸雾绕,对洛洛山发动总攻。
洛洛山西边背靠悬崖,最是险峻。你派出六指、麻袋、小东西三名心腹率人分别围住东、南、北三个方向,你亲自带领一批秘密培养的精英部队从西面悬崖攀援而上,打他张麻子一个釜底抽薪。
六指,我率领部队先爬上西面悬崖,蛰伏在草丛中。待我准备好之后,我会给你发出信号。一会你收到我给你的信号后,先从东边佯攻吸引火力,不求杀敌数量,但一定要把声势搞大,并尽可能减少弟兄们的伤亡。洛洛山此时没有防备,慌乱中必定派出大部队迎战,你只要拖住洛洛山上的大部队十分钟即可。待大部分敌人到你那里之后,你给麻袋、小东西一人一个信号,麻袋和小东西收到信号之后,从南北两面冲上去,我会在西面配合你们。这次,咱们一定要一口气端了张麻子的老巢!
大哥,六指收到!西面凶险,大哥一定要好生小心!
麻袋收到,南面交给我,我一定杀他们个片甲不留!
...
小东西,你那边怎么样?
老大...抱歉,我没怎么听明白,太多信号了,我脑子有点乱了。
我再给你们解释一遍,我和我带领的部队会先爬上西面悬崖,六指、麻袋和小东西你们三个先在三面蛰伏,你们每个人都需要收到一个信号才行动。我爬上悬崖后,会给六指一个信号,六指收到信号后开始从东边佯攻吸引火力。等火力被吸引过去后,六指给麻袋和小东西一人一个信号,这时麻袋和小东西你们两个再从南北两面行动,我会在西面和你们里应外合。听明白了吗?
老大,明白了!
老大,明白了!
...
小东西,你还有什么问题吗?
老大,我以前是个破写代码的,我敲了一份伪代码,你看下是不是这样的,我觉得我们四个部队就像四个线程一样。
你拿过小东西递过来的高端轻奢笔记本 Macbook Pro,看到小东西写了一份这样的代码:
Java 实现
publicclassFighting {publicstaticvoidmain(String[] args) {new Thread(() -> {// 老大的线程 boss(); }).start();new Thread(() -> {// 六指的线程 sixFingers(); }).start();new Thread(() -> {// 麻袋的线程 bag(); }).start();new Thread(() -> {// 小东西的线程 smallThing(); }).start(); }privatestaticvoidboss() { System.out.println("老大率领部队正在洛洛山西面爬悬崖...");// 模拟爬悬崖耗时 Thread.sleep(1000); System.out.println("老大爬上了西面山头!"); 老大给六指一个信号,通知六指行动 }privatestaticvoidsixFingers() { 六指正在等待信号,收到信号后才开始执行下面的操作 System.out.println("六指收到了信号,开始佯攻!");// 模拟佯攻吸引火力耗时 Thread.sleep(1000); System.out.println("洛洛山大部队已被吸引过来!"); 六指给麻袋一个信号 六指给小东西一个信号 }privatestaticvoidbag() { 麻袋正在等待信号,收到信号后才开始执行下面的操作 System.out.println("麻袋收到了信号,从南面冲上去!"); }privatestaticvoidsmallThing() { 小东西正在等待信号,收到信号后才开始执行下面的操作 System.out.println("小东西收到了信号,从北面冲上去!"); }}
没错,我们的计划就是这样的,但你丫的当初信号量没学好吧,还要靠伪代码。这玩意儿用信号量写就可以了。信号量在 Java 中对应 Semaphore 类,正在等待信号就是 acquire() 方法,给别人发送信号就是 release() 方法。让我来给你改成真实代码。
你端着电脑,噼里啪啦敲出了下面的代码。
Java 实现
publicclassFighting {// 给六指的信号privatestatic Semaphore semaphoreToSixFingers = new Semaphore(0);// 给麻袋的信号privatestatic Semaphore semaphoreToBag = new Semaphore(0);// 给小东西的信号privatestatic Semaphore semaphoreToSmallThing = new Semaphore(0);publicstaticvoidmain(String[] args) {new Thread(() -> {try {// 老大的线程 boss(); } catch (InterruptedException e) { e.printStackTrace(); } }).start();new Thread(() -> {try {// 六指的线程 sixFingers(); } catch (InterruptedException e) { e.printStackTrace(); } }).start();new Thread(() -> {try {// 麻袋的线程 bag(); } catch (InterruptedException e) { e.printStackTrace(); } }).start();new Thread(() -> {try {// 小东西的线程 smallThing(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); }privatestaticvoidboss() throws InterruptedException { System.out.println("老大率领部队正在洛洛山西面爬悬崖..."); Thread.sleep(1000); System.out.println("老大爬上了西面山头!"); System.out.println("老大给六指一个信号"); semaphoreToSixFingers.release(); }privatestaticvoidsixFingers() throws InterruptedException { semaphoreToSixFingers.acquire(); System.out.println("六指收到了信号,开始佯攻!"); Thread.sleep(1000); System.out.println("洛洛山大部队已被吸引过来!"); System.out.println("六指给麻袋和小东西一人一个信号"); semaphoreToBag.release(); semaphoreToSmallThing.release(); }privatestaticvoidbag() throws InterruptedException { semaphoreToSmallThing.acquire(); System.out.println("麻袋收到了信号,从南面冲上去!"); }privatestaticvoidsmallThing() throws InterruptedException { semaphoreToBag.acquire(); System.out.println("小东西收到了信号,从北面冲上去!"); }}
运行程序,输出如下:
老大率领部队正在洛洛山西面爬悬崖...老大爬上了西面山头!老大给六指一个信号六指收到了信号,开始佯攻!洛洛山大部队已被吸引过来!六指给麻袋和小东西一人一个信号麻袋收到了信号,从南面冲上去!小东西收到了信号,从北面冲上去!
“老大🐂🍺!我明白了!
信号量就是用来控制线程执行顺序的,不会用的话回去好好练练去!
老大,我也想练啊,可是我不知道在哪里练习,咱平时打家劫舍也见不到。
力扣上不是有多线程的题吗?你去刷一遍就会了。
说着你们打开了力扣官网,在题库点开了多线程题(点击查看)
我给你讲一道简单题和一道中等题,其他题你回去慢慢练。
力扣 1114. 按序打印(点击查看题目)
我们提供了一个类:
Java 实现
publicclassFoo {publicvoidone() { print("one"); }publicvoidtwo() { print("two"); }publicvoidthree() { print("three"); }}
三个不同的线程将会共用一个 Foo 实例。
线程 A 将会调用 one() 方法
线程 B 将会调用 two() 方法
线程 C 将会调用 three() 方法
请设计修改程序,以确保 two() 方法在 one() 方法之后被执行,three() 方法在 two() 方法之后被执行。
这是一道简单题,分析题目可知,我们只需要两个信号量就可以完成了。
  • 先让打印 "two" 和打印 "three" 的线程等待信号
  • "one" 打印完成后,通知 "two" 开始打印
  • "two" 打印完成后,通知 "three" 开始打印
Java 代码实现如下:
import java.util.concurrent.Semaphore;classFoo{private Semaphore sem2 = new Semaphore(0);private Semaphore sem3 = new Semaphore(0);publicFoo(){ }publicvoidfirst(Runnable printFirst)throws InterruptedException {// printFirst.run() outputs "first". Do not change or remove this line. printFirst.run();// 通知 two 开始打印 sem2.release(); }publicvoidsecond(Runnable printSecond)throws InterruptedException {// 等待信号 sem2.acquire();// printSecond.run() outputs "second". Do not change or remove this line. printSecond.run();// 通知 three 开始打印 sem3.release(); }publicvoidthird(Runnable printThird)throws InterruptedException {// 等待信号 sem3.acquire();// printThird.run() outputs "third". Do not change or remove this line. printThird.run(); }}
力扣 1115. 交替打印FooBar(点击查看题目) 
我们提供一个类:
Java 实现
classFooBar {publicvoidfoo() {for (int i = 0; i < n; i++) { print("foo"); } }publicvoidbar() {for (int i = 0; i < n; i++) { print("bar"); } }}
两个不同的线程将会共用一个 FooBar 实例。其中一个线程将会调用 foo() 方法,另一个线程将会调用 bar() 方法。
请设计修改程序,以确保 "foobar" 被输出 n 次。
这是一道中等题,我们需要控制 "foo"、"bar" 的输出顺序,思考一下可以发现,我们仍然只需要使用两个信号量。
  • 先让打印 "bar" 的线程等待信号
  • 打印 "foo" 完成后,通知 "bar" 开始打印,然后打印 "foo" 的线程等待信号
  • 打印 "bar" 完成后,通知 "foo" 开始打印,然后打印 "bar" 的线程等待信号
  • 这样交替执行,直到输出 n 次 "foobar"
Java 代码实现如下:
import java.util.concurrent.Semaphore;classFooBar{privateint n;private Semaphore semFoo = new Semaphore(0);private Semaphore semBar = new Semaphore(0);publicFooBar(int n){this.n = n; }publicvoidfoo(Runnable printFoo)throws InterruptedException {for (int i = 0; i < n; i++) {// printFoo.run() outputs "foo". Do not change or remove this line. printFoo.run();// 通知 "bar" 开始打印 semBar.release();// 等待信号 semFoo.acquire(); } }publicvoidbar(Runnable printBar)throws InterruptedException {for (int i = 0; i < n; i++) {// 等待信号 semBar.acquire();// printBar.run() outputs "bar". Do not change or remove this line. printBar.run();// 通知 "foo" 开始打印 semFoo.release(); } }}
就是这样,其他的多线程题也是类似的,只要用信号量就可以轻松解决。
老大,初始化的代码里, new Semaphore(0) 里面的 0 是什么意思呢?
意思就是初始的时候有多少个信号,如果已经有至少 1 个信号,acquire() 方法就无需等待,而是直接消耗一个信号继续执行。也就是说每调用一次 release() 方法,就会添加一个信号,每调用一次 acquire() 方法,就会减少一个信号。当信号数量为 0 时,acquire() 方法就会等待。所以上面的代码也可以改写成下面这样,显得 foo 方法和 bar 方法的格式看起来更加统一。
Java 实现
classFooBar{privateint n;// 初始时有一个 Foo 信号private Semaphore semFoo = new Semaphore(1);private Semaphore semBar = new Semaphore(0);publicFooBar(int n){this.n = n; }publicvoidfoo(Runnable printFoo)throws InterruptedException {for (int i = 0; i < n; i++) {// 等待信号,第一次的时候,因为已经有一个 Foo 信号,所以这里会直接消耗一个信号,继续执行 semFoo.acquire();// printFoo.run() outputs "foo". Do not change or remove this line. printFoo.run();// 通知 "bar" 开始打印 semBar.release(); } }publicvoidbar(Runnable printBar)throws InterruptedException {for (int i = 0; i < n; i++) {// 等待信号 semBar.acquire();// printBar.run() outputs "bar". Do not change or remove this line. printBar.run();// 通知 "foo" 开始打印 semFoo.release(); } }}
这次我是真听明白了!信号量真是解决多线程问题的利器,谢谢老大!
你丫的回去一定要把多线程的题目好好练完!
关于「信号量解决多线程问题」的知识点你听明白了嘛?可以在评论区给我们留言哦~
BY / 
本文作者:Alpinist Wang
编辑&版式:霍霍
声明:本文归 “力扣” 版权所有,如需转载请联系。文章封面图和文中部分图片来源于网络,如有侵权联系删除。
推荐阅读
点个在看,少个 bug👇
继续阅读
阅读原文