月圓之夜,洛洛山。
洛洛山坐落於多線程王國的一個偏遠小鎮,山上綠樹環繞,風光靈秀。踏着蜿蜒的小路循跡而上,花香、鳥啼、青柳、蟲鳴,共同演奏者一曲天地之歌,大自然的鬼斧神工和洛洛山大當家張麻子的笑聲一樣豪放不羈。張麻子佔領洛洛山十年有餘,寨子創立之初,他便定下規矩:“三不劫”——老少不劫、農民不劫、婦人不劫。只對路過的富商、官宦等有錢有勢的人家下手,收取過路費用。在他多年用心經營之下,寨子已小有規模。寨中壯丁上千許,庫內酒錢上萬貫。張麻子作爲大當家,正是意氣風發之時,問蒼茫大地,我主沉浮!終日飲酒作樂,好不快活。
人到無求品自高,張麻子在洛洛山勢力足夠大時,便很少再管束寨子中的事,漸漸地任其自由生長。洛洛山的手下們長期過着優渥的生活,不免沾染上些紈絝之氣。寨子從最開始的劫富濟貧發展到後來,反倒成了一方惡霸,手下的人不規矩,老少劫、農民劫、婦人也劫,鎮上人民苦不堪言。張麻子本人卻越來越不問世事,心懷通達,見到誰都是滿面笑容,讓人如沐春風之中。
不過,在你眼裏張麻子的笑就不是那麼讓人歡喜了。男兒何不帶吳鉤?作爲一名有志青年,你也曾縱馬橫刀,決心佔山爲王。然而終敵不過洛洛山傳承已久,底蘊深厚。最終你只能在次一等的皮皮山安營紮寨,皮皮山地勢險峻,易守難攻。物產又極其匱乏,張麻子不屑於爲皮皮山上的蠅頭小利大動干戈,這些年來,兩座山頭倒也相安無事。
然而山珍海味擺在眼前,誰又能將就得了鹹菜蘿蔔。你早已在心中暗暗發誓,勢必佔領洛洛山頭!正值十五月圓夜,謀事月黑風高時。你率領手下八百餘人悄然圍攻洛洛山,之所以選擇十五是因爲你知道月兒越明,星星越稀少,若再遇上烏雲遮蔽月色,端的是伸手不見五指,正是動手的好時機。你決定趁着今夜雲蒸霧繞,對洛洛山發動總攻。
洛洛山西邊背靠懸崖,最是險峻。你派出六指、麻袋、小東西三名心腹率人分別圍住東、南、北三個方向,你親自帶領一批祕密培養的精英部隊從西面懸崖攀援而上,決心打他張麻子一個釜底抽薪。
“六指,我率領部隊先爬上西面懸崖,蟄伏在草叢中。待我準備好之後,我會給你發出信號。一會你收到我給你的信號後,先從東邊佯攻吸引火力,不求殺敵數量,但一定要把聲勢搞大,並儘可能減少弟兄們的傷亡。洛洛山此時沒有防備,慌亂中必定派出大部隊迎戰,你只要拖住洛洛山上的大部隊十分鐘即可。待大部分敵人到你那裏之後,你給麻袋、小東西一人一個信號,麻袋和小東西收到信號之後,從南北兩面衝上去,我會在西面配合你們。這次,咱們一定要一口氣端了張麻子的老巢!”
六指:“大哥,六指收到!西面兇險,大哥一定要好生小心!”
麻袋:“麻袋收到,南面交給我,我一定殺他們個片甲不留!”
小東西:“…”
“小東西,你那邊怎麼樣?”
小東西:“老大…抱歉,我沒怎麼聽明白,太多信號了,我腦子有點亂了。”
“我再給你們解釋一遍,我和我帶領的部隊會先爬上西面懸崖,六指、麻袋和小東西你們三個先在三面蟄伏,你們每個人都需要收到一個信號纔行動。我爬上懸崖後,會給六指一個信號,六指收到信號後開始從東邊佯攻吸引火力。等火力被吸引過去後,六指給麻袋和小東西一人一個信號,這時麻袋和小東西你們兩個再從南北兩面行動,我會在西面和你們裏應外合。聽明白了嗎?”
六指:“老大,明白了!”
麻袋:“老大,明白了!”
小東西:“…”
“小東西,你還有什麼問題嗎?”
小東西:“老大,我以前是個破寫代碼的,我敲了一份僞代碼,你看下是不是這樣的,我覺得我們四個部隊就像四個線程一樣。”
你拿過小東西遞過來的高端輕奢筆記本 Macbook Pro,看到小東西寫了一份這樣的代碼:
public class Fighting {
public static void main(String[] args) {
new Thread(() -> {
// 老大的線程
boss();
}).start();
new Thread(() -> {
// 六指的線程
sixFingers();
}).start();
new Thread(() -> {
// 麻袋的線程
bag();
}).start();
new Thread(() -> {
// 小東西的線程
smallThing();
}).start();
}
private static void boss() {
System.out.println("老大率領部隊正在洛洛山西面爬懸崖...");
// 模擬爬懸崖耗時
Thread.sleep(1000);
System.out.println("老大爬上了西面山頭!");
老大給六指一個信號,通知六指行動
}
private static void sixFingers() {
六指正在等待信號,收到信號後纔開始執行下面的操作
System.out.println("六指收到了信號,開始佯攻!");
// 模擬佯攻吸引火力耗時
Thread.sleep(1000);
System.out.println("洛洛山大部隊已被吸引過來!");
六指給麻袋一個信號
六指給小東西一個信號
}
private static void bag() {
麻袋正在等待信號,收到信號後纔開始執行下面的操作
System.out.println("麻袋收到了信號,從南面衝上去!");
}
private static void smallThing() {
小東西正在等待信號,收到信號後纔開始執行下面的操作
System.out.println("小東西收到了信號,從北面衝上去!");
}
}
“沒錯,我們的計劃就是這樣的,但你丫的當初信號量沒學好吧,還要靠僞代碼。這玩意兒用信號量寫就可以了。信號量在 Java 中對應 Semaphore 類,正在等待信號就是 acquire() 方法,給別人發送信號就是 release() 方法。讓我來給你改成真實代碼。”
你端着電腦,噼裏啪啦敲出了下面的代碼。
public class Fighting {
// 給六指的信號
private static Semaphore semaphoreToSixFingers = new Semaphore(0);
// 給麻袋的信號
private static Semaphore semaphoreToBag = new Semaphore(0);
// 給小東西的信號
private static Semaphore semaphoreToSmallThing = new Semaphore(0);
public static void main(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();
}
private static void boss() throws InterruptedException {
System.out.println("老大率領部隊正在洛洛山西面爬懸崖...");
Thread.sleep(1000);
System.out.println("老大爬上了西面山頭!");
System.out.println("老大給六指一個信號");
semaphoreToSixFingers.release();
}
private static void sixFingers() throws InterruptedException {
semaphoreToSixFingers.acquire();
System.out.println("六指收到了信號,開始佯攻!");
Thread.sleep(1000);
System.out.println("洛洛山大部隊已被吸引過來!");
System.out.println("六指給麻袋和小東西一人一個信號");
semaphoreToBag.release();
semaphoreToSmallThing.release();
}
private static void bag() throws InterruptedException {
semaphoreToSmallThing.acquire();
System.out.println("麻袋收到了信號,從南面衝上去!");
}
private static void smallThing() throws InterruptedException {
semaphoreToBag.acquire();
System.out.println("小東西收到了信號,從北面衝上去!");
}
}
運行程序,輸出如下:
老大率領部隊正在洛洛山西面爬懸崖...
老大爬上了西面山頭!
老大給六指一個信號
六指收到了信號,開始佯攻!
洛洛山大部隊已被吸引過來!
六指給麻袋和小東西一人一個信號
麻袋收到了信號,從南面衝上去!
小東西收到了信號,從北面衝上去!
小東西:“老大🐂🍺!我明白了!”
“信號量就是用來控制線程執行順序的,不會用的話回去好好練練去!”
小東西:“老大,我也想練啊,可是我不知道在哪裏練習,咱平時打家劫舍也見不到。”
“力扣上不是有多線程的題嗎?你去刷一遍就會了。”
說着你們打開了力扣官網,在題庫
中點開了多線程
題:https://leetcode-cn.com/problemset/concurrency/
“我給你講一道簡單題和一道中等題,其他題你回去慢慢練。”
1114. 按序打印
我們提供了一個類:
public class Foo {
public void one() { print("one"); }
public void two() { print("two"); }
public void three() { print("three"); }
}
三個不同的線程將會共用一個 Foo 實例。
線程 A 將會調用 one() 方法
線程 B 將會調用 two() 方法
線程 C 將會調用 three() 方法
請設計修改程序,以確保 two() 方法在 one() 方法之後被執行,three() 方法在 two() 方法之後被執行。
這是一道簡單題,分析題目可知,我們只需要兩個信號量就可以完成了。
- 先讓打印 “two” 和打印 “three” 的線程等待信號
- “one” 打印完成後,通知 “two” 開始打印
- “two” 打印完成後,通知 “three” 開始打印
代碼實現如下:
import threading
class Foo:
def __init__(self):
self.sem2 = threading.Semaphore(0)
self.sem3 = threading.Semaphore(0)
def first(self, printFirst: 'Callable[[], None]') -> None:
printFirst()
# 通知 two 開始打印
self.sem2.release()
def second(self, printSecond: 'Callable[[], None]') -> None:
# 等待信號
self.sem2.acquire()
printSecond()
# 通知 three 開始打印
self.sem3.release()
def third(self, printThird: 'Callable[[], None]') -> None:
# 等待信號
self.sem3.acquire()
printThird()
1115. 交替打印FooBar
我們提供一個類:
class FooBar {
public void foo() {
for (int i = 0; i < n; i++) {
print("foo");
}
}
public void bar() {
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”
代碼實現如下:
import java.util.concurrent.Semaphore;
class FooBar {
private int n;
private Semaphore semFoo = new Semaphore(0);
private Semaphore semBar = new Semaphore(0);
public FooBar(int n) {
this.n = n;
}
public void foo(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();
}
}
public void bar(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 方法的格式看起來更加統一。”
class FooBar {
private int n;
// 初始時有一個 Foo 信號
private Semaphore semFoo = new Semaphore(1);
private Semaphore semBar = new Semaphore(0);
public FooBar(int n) {
this.n = n;
}
public void foo(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();
}
}
public void bar(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();
}
}
}
小東西:“這次我是真聽明白了!信號量真是解決多線程問題的利器,謝謝老大!”
“你丫的回去一定要把多線程的題目好好練完!”