程式設計師生存指南之「打家劫舍」

力扣leetcode 發佈 2020-06-10T03:58:26+00:00

// printBar.run outputs 「bar」. Do not change or remove this line.


月圓之夜,洛洛山。

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

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

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

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

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

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

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

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

小東西:「...」

「小東西,你那邊怎麼樣?」

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

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

六指:「老大,明白了!」

麻袋:「老大,明白了!」

小東西:「...」

「小東西,你還有什麼問題嗎?」

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

你拿過小東西遞過來的高端輕奢筆記本 Macbook Pro,看到小東西寫了一份這樣的代碼:

Java 實現

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() 方法。讓我來給你改成真實代碼。」

你端著電腦,噼里啪啦敲出了下面的代碼。

Java 實現

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("小東西收到了信號,從北面衝上去!");
    }
}

運行程序,輸出如下:

老大率領部隊正在洛洛山西面爬懸崖...
老大爬上了西面山頭!
老大給六指一個信號

六指收到了信號,開始佯攻!
洛洛山大部隊已被吸引過來!
六指給麻袋和小東西一人一個信號

麻袋收到了信號,從南面衝上去!

小東西收到了信號,從北面衝上去!

小東西:「老大 NB!我明白了!」

「信號量就是用來控制線程執行順序的,不會用的話回去好好練練去!」

小東西:「老大,我也想練啊,可是我不知道在哪裡練習,咱平時打家劫舍也見不到。」

「力扣上不是有多線程的題嗎?你去刷一遍就會了。」

說著你們打開了力扣官網,在題庫中點開了多線程題:https://leetcode-cn.com/problemset/concurrency/

「我給你講一道簡單題和一道中等題,其他題你回去慢慢練。」


力扣 1114. 按序列印「連結」(點擊查看題目)

我們提供了一個類:

Java 實現

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" 開始列印

Java 代碼實現如下:

import java.util.concurrent.Semaphore;
​
​class Foo {
​
    private Semaphore sem2 = new Semaphore(0);
    private Semaphore sem3 = new Semaphore(0);
​
    public Foo() {
    }
​
    public void first(Runnable printFirst) throws InterruptedException {
        // printFirst.run() outputs "first". Do not change or remove this line.
        printFirst.run();
        // 通知 two 開始列印
        sem2.release();
    }
​
    public void second(Runnable printSecond) throws InterruptedException {
        // 等待信號
        sem2.acquire();
        // printSecond.run() outputs "second". Do not change or remove this line.
        printSecond.run();
        // 通知 three 開始列印
        sem3.release();
    }
​
    public void third(Runnable printThird) throws InterruptedException {
        // 等待信號
        sem3.acquire();
        // printThird.run() outputs "third". Do not change or remove this line.
        printThird.run();
    }
}


力扣 1115. 交替列印FooBar 「連結」(點擊查看題目)

我們提供一個類:

Java 實現

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"

Java 代碼實現如下:

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 方法的格式看起來更加統一。」

Java 實現

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();
        }
    }
}

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

「你回去一定要把多線程的題目好好練完!」

關於「信號量解決多線程問題」的知識你點聽明白了嘛?可以在評論區給我們留言哦~


本文作者:Alpinist Wang

聲明:本文歸 「力扣」 版權所有,如需轉載請聯繫。文章封面圖來源於網絡,如有侵權聯繫刪除。

關鍵字: