攻山记 —— 一招搞定力扣上的多线程题

月圆之夜,洛洛山。

洛洛山坐落于多线程王国的一个偏远小镇,山上绿树环绕,风光灵秀。踏着蜿蜒的小路循迹而上,花香、鸟啼、青柳、虫鸣,共同演奏者一曲天地之歌,大自然的鬼斧神工和洛洛山大当家张麻子的笑声一样豪放不羁。张麻子占领洛洛山十年有余,寨子创立之初,他便定下规矩:“三不劫”——老少不劫、农民不劫、妇人不劫。只对路过的富商、官宦等有钱有势的人家下手,收取过路费用。在他多年用心经营之下,寨子已小有规模。寨中壮丁上千许,库内酒钱上万贯。张麻子作为大当家,正是意气风发之时,问苍茫大地,我主沉浮!终日饮酒作乐,好不快活。

人到无求品自高,张麻子在洛洛山势力足够大时,便很少再管束寨子中的事,渐渐地任其自由生长。洛洛山的手下们长期过着优渥的生活,不免沾染上些纨绔之气。寨子从最开始的劫富济贫发展到后来,反倒成了一方恶霸,手下的人不规矩,老少劫、农民劫、妇人也劫,镇上人民苦不堪言。张麻子本人却越来越不问世事,心怀通达,见到谁都是满面笑容,让人如沐春风之中。

不过,在你眼里张麻子的笑就不是那么让人欢喜了。男儿何不带吴钩?作为一名有志青年,你也曾纵马横刀,决心占山为王。然而终敌不过洛洛山传承已久,底蕴深厚。最终你只能在次一等的皮皮山安营扎寨,皮皮山地势险峻,易守难攻。物产又极其匮乏,张麻子不屑于为皮皮山上的蝇头小利大动干戈,这些年来,两座山头倒也相安无事。

然而山珍海味摆在眼前,谁又能将就得了咸菜萝卜。你早已在心中暗暗发誓,势必占领洛洛山头!正值十五月圆夜,谋事月黑风高时。你率领手下八百余人悄然围攻洛洛山,之所以选择十五是因为你知道月儿越明,星星越稀少,若再遇上乌云遮蔽月色,端的是伸手不见五指,正是动手的好时机。你决定趁着今夜云蒸雾绕,对洛洛山发动总攻。

洛洛山西边背靠悬崖,最是险峻。你派出六指、麻袋、小东西三名心腹率人分别围住东、南、北三个方向,你亲自带领一批秘密培养的精英部队从西面悬崖攀援而上,决心打他张麻子一个釜底抽薪。

“六指,我率领部队先爬上西面悬崖,蛰伏在草丛中。待我准备好之后,我会给你发出信号。一会你收到我给你的信号后,先从东边佯攻吸引火力,不求杀敌数量,但一定要把声势搞大,并尽可能减少弟兄们的伤亡。洛洛山此时没有防备,慌乱中必定派出大部队迎战,你只要拖住洛洛山上的大部队十分钟即可。待大部分敌人到你那里之后,你给麻袋、小东西一人一个信号,麻袋和小东西收到信号之后,从南北两面冲上去,我会在西面配合你们。这次,咱们一定要一口气端了张麻子的老巢!”

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

麻袋:“麻袋收到,南面交给我,我一定杀他们个片甲不留!”

小东西:“…”

“小东西,你那边怎么样?”

小东西:“老大…抱歉,我没怎么听明白,太多信号了,我脑子有点乱了。”

“我再给你们解释一遍,我和我带领的部队会先爬上西面悬崖,六指、麻袋和小东西你们三个先在三面蛰伏,你们每个人都需要收到一个信号才行动。我爬上悬崖后,会给六指一个信号,六指收到信号后开始从东边佯攻吸引火力。等火力被吸引过去后,六指给麻袋和小东西一人一个信号,这时麻袋和小东西你们两个再从南北两面行动,我会在西面和你们里应外合。听明白了吗?”

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

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

小东西:“…”

“小东西,你还有什么问题吗?”

小东西:“老大,我以前是个破写代码的,我敲了一份伪代码,你看下是不是这样的,我觉得我们四个部队就像四个线程一样。”

你拿过小东西递过来的高端轻奢笔记本 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();
        }
    }
}

小东西:“这次我是真听明白了!信号量真是解决多线程问题的利器,谢谢老大!”

“你丫的回去一定要把多线程的题目好好练完!”

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