攻山記 —— 一招搞定力扣上的多線程題

月圓之夜,洛洛山。

洛洛山坐落於多線程王國的一個偏遠小鎮,山上綠樹環繞,風光靈秀。踏着蜿蜒的小路循跡而上,花香、鳥啼、青柳、蟲鳴,共同演奏者一曲天地之歌,大自然的鬼斧神工和洛洛山大當家張麻子的笑聲一樣豪放不羈。張麻子佔領洛洛山十年有餘,寨子創立之初,他便定下規矩:“三不劫”——老少不劫、農民不劫、婦人不劫。只對路過的富商、官宦等有錢有勢的人家下手,收取過路費用。在他多年用心經營之下,寨子已小有規模。寨中壯丁上千許,庫內酒錢上萬貫。張麻子作爲大當家,正是意氣風發之時,問蒼茫大地,我主沉浮!終日飲酒作樂,好不快活。

人到無求品自高,張麻子在洛洛山勢力足夠大時,便很少再管束寨子中的事,漸漸地任其自由生長。洛洛山的手下們長期過着優渥的生活,不免沾染上些紈絝之氣。寨子從最開始的劫富濟貧發展到後來,反倒成了一方惡霸,手下的人不規矩,老少劫、農民劫、婦人也劫,鎮上人民苦不堪言。張麻子本人卻越來越不問世事,心懷通達,見到誰都是滿面笑容,讓人如沐春風之中。

不過,在你眼裏張麻子的笑就不是那麼讓人歡喜了。男兒何不帶吳鉤?作爲一名有志青年,你也曾縱馬橫刀,決心佔山爲王。然而終敵不過洛洛山傳承已久,底蘊深厚。最終你只能在次一等的皮皮山安營紮寨,皮皮山地勢險峻,易守難攻。物產又極其匱乏,張麻子不屑於爲皮皮山上的蠅頭小利大動干戈,這些年來,兩座山頭倒也相安無事。

然而山珍海味擺在眼前,誰又能將就得了鹹菜蘿蔔。你早已在心中暗暗發誓,勢必佔領洛洛山頭!正值十五月圓夜,謀事月黑風高時。你率領手下八百餘人悄然圍攻洛洛山,之所以選擇十五是因爲你知道月兒越明,星星越稀少,若再遇上烏雲遮蔽月色,端的是伸手不見五指,正是動手的好時機。你決定趁着今夜雲蒸霧繞,對洛洛山發動總攻。

洛洛山西邊背靠懸崖,最是險峻。你派出六指、麻袋、小東西三名心腹率人分別圍住東、南、北三個方向,你親自帶領一批祕密培養的精英部隊從西面懸崖攀援而上,決心打他張麻子一個釜底抽薪。

“六指,我率領部隊先爬上西面懸崖,蟄伏在草叢中。待我準備好之後,我會給你發出信號。一會你收到我給你的信號後,先從東邊佯攻吸引火力,不求殺敵數量,但一定要把聲勢搞大,並儘可能減少弟兄們的傷亡。洛洛山此時沒有防備,慌亂中必定派出大部隊迎戰,你只要拖住洛洛山上的大部隊十分鐘即可。待大部分敵人到你那裏之後,你給麻袋、小東西一人一個信號,麻袋和小東西收到信號之後,從南北兩面衝上去,我會在西面配合你們。這次,咱們一定要一口氣端了張麻子的老巢!”

六指:“大哥,六指收到!西面兇險,大哥一定要好生小心!”

麻袋:“麻袋收到,南面交給我,我一定殺他們個片甲不留!”

小東西:“…”

“小東西,你那邊怎麼樣?”

小東西:“老大…抱歉,我沒怎麼聽明白,太多信號了,我腦子有點亂了。”

“我再給你們解釋一遍,我和我帶領的部隊會先爬上西面懸崖,六指、麻袋和小東西你們三個先在三面蟄伏,你們每個人都需要收到一個信號纔行動。我爬上懸崖後,會給六指一個信號,六指收到信號後開始從東邊佯攻吸引火力。等火力被吸引過去後,六指給麻袋和小東西一人一個信號,這時麻袋和小東西你們兩個再從南北兩面行動,我會在西面和你們裏應外合。聽明白了嗎?”

六指:“老大,明白了!”

麻袋:“老大,明白了!”

小東西:“…”

“小東西,你還有什麼問題嗎?”

小東西:“老大,我以前是個破寫代碼的,我敲了一份僞代碼,你看下是不是這樣的,我覺得我們四個部隊就像四個線程一樣。”

你拿過小東西遞過來的高端輕奢筆記本 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();
        }
    }
}

小東西:“這次我是真聽明白了!信號量真是解決多線程問題的利器,謝謝老大!”

“你丫的回去一定要把多線程的題目好好練完!”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章