LeetCode_多线程

本文介绍了并发编程中的经典问题——哲学家进餐问题,并展示了使用Java的ReentrantLock和Semaphore解决此问题的策略。此外,还探讨了交替打印字符串、H2O生成、打印零与奇偶数、交替打印FooBar以及按序打印等并发控制场景,通过具体的Java代码展示了如何利用并发工具保证线程间的正确同步和执行顺序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

哲学家进餐

1 题目描述

5 个沉默寡言的哲学家围坐在圆桌前,每人面前一盘意面。叉子放在哲学家之间的桌面上。(5 个哲学家,5 根叉子)

所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的叉子才能吃到面,而同一根叉子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把叉子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的叉子,但在没有同时拿到左右叉子时不能进食。

假设面的数量没有限制,哲学家也能随便吃,不需要考虑吃不吃得下。

设计一个进餐规则(并行算法)使得每个哲学家都不会挨饿;也就是说,在没有人知道别人什么时候想吃东西或思考的情况下,每个哲学家都可以在吃饭和思考之间一直交替下去。

在这里插入图片描述
哲学家从 0 到 4 按 顺时针 编号。请实现函数 void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork):

  • philosopher 哲学家的编号。
  • pickLeftFork 和 pickRightFork 表示拿起左边或右边的叉子。
  • eat 表示吃面。
  • putLeftFork 和 putRightFork 表示放下左边或右边的叉子。
  • 由于哲学家不是在吃面就是在想着啥时候吃面,所以思考这个方法没有对应的回调。

给你 5 个线程,每个都代表一个哲学家,请你使用类的同一个对象来模拟这个过程。在最后一次调用结束之前,可能会为同一个哲学家多次调用该函数。

2 解题(Java)

解题思路

这道题本质上其实是想考察如何避免死锁。

死锁产生的必要条件
在这里插入图片描述
(方框表示资源,圆圈表示进程,资源指向进程表示资源已经分配给进程,进程指向资源表示进程请求获取资源)

  • 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束;
  • 占有和等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源;
  • 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放;
  • 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源;

解题

最多只允许 4 个哲学家去持有叉子,可保证至少有 1 个哲学家能吃上意大利面(即获得到 2 个叉子)。

在这里插入图片描述

代码

class DiningPhilosophers {
    // 5个ReentrantLock代表5个叉子
    private ReentrantLock[] lockList = {
        new ReentrantLock(),
        new ReentrantLock(),
        new ReentrantLock(),
        new ReentrantLock(),
        new ReentrantLock()
    };
    
    // 限制最多只有4个哲学家去持有叉子,则至少有一个哲学家可以吃到面
    private Semaphore eatLimit = new Semaphore(4);

    public DiningPhilosophers() {

    }

    // call the run() method of any runnable to execute its code
    public void wantsToEat(int philosopher,
                            Runnable pickLeftFork,
                            Runnable pickRightFork,
                            Runnable eat,
                            Runnable putLeftFork,
                            Runnable putRightFork) throws InterruptedException {

        eatLimit.acquire(); // 限制的人数-1

        int rightFork = philosopher; // 右边叉子的编号
        int leftFork = (philosopher + 1) % 5; // 左边叉子的编号

        lockList[rightFork].lock(); // 拿起右边的叉子
        lockList[leftFork].lock(); // 拿起左边的叉子

        pickRightFork.run(); // 拿起右边叉子的具体执行
        pickLeftFork.run(); // 拿起左边叉子的具体执行

        eat.run(); // 吃意大利面的具体执行

        putRightFork.run(); // 放下右边叉子的具体执行
        putLeftFork.run(); // 放下左边叉子的具体执行

        lockList[rightFork].unlock(); // 放下右边的叉子
        lockList[leftFork].unlock(); // 放下左边的叉子

        eatLimit.release(); // 限制的人数+1
    }
}

交替打印字符串

1 题目描述

编写一个可以从 1 到 n 输出代表这个数字的字符串的程序,但是:

  • 如果这个数字可以被 3 整除,输出 “fizz”。
  • 如果这个数字可以被 5 整除,输出 “buzz”。
  • 如果这个数字可以同时被 3 和 5 整除,输出 “fizzbuzz”。

例如,当 n = 15,输出: 1, 2, fizz, 4, buzz, fizz, 7, 8, fizz, buzz, 11, fizz, 13, 14, fizzbuzz。

假设有这么一个类:

class FizzBuzz {
  public FizzBuzz(int n) { ... }               // constructor
  public void fizz(printFizz) { ... }          // only output "fizz"
  public void buzz(printBuzz) { ... }          // only output "buzz"
  public void fizzbuzz(printFizzBuzz) { ... }  // only output "fizzbuzz"
  public void number(printNumber) { ... }      // only output the numbers
}

请你实现一个有四个线程的多线程版 FizzBuzz, 同一个 FizzBuzz 实例会被如下四个线程使用:

  1. 线程A将调用 fizz() 来判断是否能被 3 整除,如果可以,则输出 fizz。
  2. 线程B将调用 buzz() 来判断是否能被 5 整除,如果可以,则输出 buzz。
  3. 线程C将调用 fizzbuzz() 来判断是否同时能被 3 和 5 整除,如果可以,则输出 fizzbuzz。
  4. 线程D将调用 number() 来实现输出既不能被 3 整除也不能被 5 整除的数字。

提示

  • 本题已经提供了打印字符串的相关方法,如 printFizz() 等,具体方法名请参考答题模板中的注释部分。

2 解题(Java)

class FizzBuzz {
    private int n;
    private CyclicBarrier cyclicBarrier = new CyclicBarrier(4);

    public FizzBuzz(int n) {
        this.n = n;
    }

    // printFizz.run() outputs "fizz".
    public void fizz(Runnable printFizz) throws InterruptedException {
        for (int i = 1; i <= n; i++) {
            if (i % 3 == 0 && i % 5 != 0) {
                printFizz.run();
            }
            try {
                cyclicBarrier.await();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    // printBuzz.run() outputs "buzz".
    public void buzz(Runnable printBuzz) throws InterruptedException {
        for (int i = 1; i <= n; i++) {
            if (i % 3 != 0 && i % 5 == 0) {
                printBuzz.run();
            }
            try {
                cyclicBarrier.await();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    // printFizzBuzz.run() outputs "fizzbuzz".
    public void fizzbuzz(Runnable printFizzBuzz) throws InterruptedException {
        for (int i = 1; i <= n; i++) {
            if (i % 3 == 0 && i % 5 == 0) {
                printFizzBuzz.run();
            }
            try {
                cyclicBarrier.await();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void number(IntConsumer printNumber) throws InterruptedException {
        for (int i = 1; i <= n; i++) {
            if (i % 3 != 0 && i % 5 != 0) {
                printNumber.accept(i);
            }
            try {
                cyclicBarrier.await();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

H2O 生成

1 题目描述

现在有两种线程,氧 oxygen 和氢 hydrogen,你的目标是组织这两种线程来产生水分子。

存在一个屏障(barrier)使得每个线程必须等候直到一个完整水分子能够被产生出来。

氢和氧线程会被分别给予 releaseHydrogen 和 releaseOxygen 方法来允许它们突破屏障。

这些线程应该三三成组突破屏障并能立即组合产生一个水分子。

你必须保证产生一个水分子所需线程的结合必须发生在下一个水分子产生之前。

换句话说:

  • 如果一个氧线程到达屏障时没有氢线程到达,它必须等候直到两个氢线程到达。
  • 如果一个氢线程到达屏障时没有其它线程到达,它必须等候直到一个氧线程和另一个氢线程到达。

书写满足这些限制条件的氢、氧线程同步代码。

示例 1:

输入: “HOH”
输出: “HHO”
解释: “HOH” 和 “OHH” 依然都是有效解。

示例 2:

输入: “OOHHHH”
输出: “HHOHHO”
解释: “HOHHHO”, “OHHHHO”, “HHOHOH”, “HOHHOH”, “OHHHOH”, “HHOOHH”, “HOHOHH” 和 “OHHOHH” 依然都是有效解。

提示

  • 输入字符串的总长将会是 3n, 1 ≤ n ≤ 50;
  • 输入字符串中的 “H” 总数将会是 2n ;
  • 输入字符串中的 “O” 总数将会是 n ;

2 解题(Java)

class H2O {

  Semaphore H = new Semaphore(2);
  Semaphore O = new Semaphore(0);

  public H2O() {

  }

  public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {

      H.acquire(1);
      
      // releaseHydrogen.run() outputs "H". Do not change or remove this line.
      releaseHydrogen.run();
      O.release(1);
  }

  public void oxygen(Runnable releaseOxygen) throws InterruptedException {

      O.acquire(2);
      
      // releaseOxygen.run() outputs "O". Do not change or remove this line.
      releaseOxygen.run();
      H.release(2);
  }
}

打印零与奇偶数

1 题目描述

假设有这么一个类:

class ZeroEvenOdd {
  public ZeroEvenOdd(int n) { ... }      // 构造函数
  public void zero(printNumber) { ... }  // 仅打印出 0
  public void even(printNumber) { ... }  // 仅打印出 偶数
  public void odd(printNumber) { ... }   // 仅打印出 奇数
}

相同的一个 ZeroEvenOdd 类实例将会传递给三个不同的线程:

  1. 线程 A 将调用 zero(),它只输出 0 。
  2. 线程 B 将调用 even(),它只输出偶数。
  3. 线程 C 将调用 odd(),它只输出奇数。

每个线程都有一个 printNumber 方法来输出一个整数。请修改给出的代码以输出整数序列 010203040506… ,其中序列的长度必须为 2n。

示例 1

输入:n = 2
输出:“0102”
说明:三条线程异步执行,其中一个调用 zero(),另一个线程调用 even(),最后一个线程调用odd()。正确的输出为 “0102”。

示例 2

输入:n = 5
输出:“0102030405”

2 解题(Java)

class ZeroEvenOdd {
    private int n;
    private Semaphore zero = new Semaphore(1);
    private Semaphore even = new Semaphore(0);
    private Semaphore odd = new Semaphore(0);
    
    public ZeroEvenOdd(int n) {
        this.n = n;
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i++) {
            zero.acquire();
            printNumber.accept(0);
            if (i % 2 == 0) {
                even.release();
            } else {
                odd.release();
            }
        }
        
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        for (int i=2; i<=n; i+=2) {
            even.acquire();
            printNumber.accept(i);
            zero.release();
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        for (int i=1; i<=n; i+=2) {
            odd.acquire();
            printNumber.accept(i);
            zero.release();
        }
    }
}

交替打印FooBar

1 题目描述

我们提供一个类:

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 次。

示例 1:

输入: n = 1
输出: “foobar”
解释: 这里有两个线程被异步启动。其中一个调用 foo() 方法, 另一个调用 bar() 方法,“foobar” 将被输出一次。

示例 2:

输入: n = 2
输出: “foobarfoobar”
解释: “foobar” 将被输出两次。

2 解题(Java)

semaphore:信号量。

semaphore.acquire();获取令牌

  1. 当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子的操作去修改同步队列的state,获取一个令牌则修改为state=state-1;
  2. 当计算出来的state<0,则代表令牌数量不足,此时会创建一个Node节点加入阻塞队列,挂起当前线程;
  3. 当计算出来的state>=0,则代表获取令牌成功;

semaphore.release();释放令牌

  1. 线程会尝试释放一个令牌,释放令牌的过程也就是把同步队列的state修改为state=state+1的过程;
  2. 释放令牌成功之后,同时会唤醒同步队列的所有阻塞的节点线程;
  3. 被唤醒的节点会重新尝试去修改state=state-1 的操作,如果state>=0则获取令牌成功,否则重新进入阻塞队列,挂起线程;
class FooBar {
    private int n;
    private Semaphore foo = new Semaphore(1);
    private Semaphore bar = 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.acquire();
            printFoo.run();
            bar.release();
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {
        for (int i=0; i<n; i++) {
            bar.acquire();
            printBar.run();
            foo.release();
        }
    }
}

按序打印

1 题目描述

我们提供了一个类:

public class Foo {
  public void first() { print("first"); }
  public void second() { print("second"); }
  public void third() { print("third"); }
}

三个不同的线程 A、B、C 将会共用一个 Foo 实例。

  • 一个将会调用 first() 方法
  • 一个将会调用 second() 方法
  • 还有一个将会调用 third() 方法

请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

示例 1:

输入: [1,2,3]
输出: “firstsecondthird”
解释: 有三个线程会被异步启动。 输入 [1,2,3] 表示线程 A将会调用 first() 方法,线程 B 将会调用 second() 方法,线程 C 将会调用 third() 方法。 正确的输出是 “firstsecondthird”。

示例 2:

输入: [1,3,2]
输出: “firstsecondthird”
解释: 输入 [1,3,2] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 third() 方法,线程 C 将会调用 second() 方法。 正确的输出是"firstsecondthird"。

提示:

  • 尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。 你看到的输入格式主要是为了确保测试的全面性。

2 解题(Java)

2.1 解法1:共享变量

思路

  1. 要求按顺序依次执行三个方法,且每个方法都在单独的线程中执行,因此为了保证线程执行顺序,可以在方法之间创建一些依赖关系;
  2. 即第二个方法必须在第一个方法之后执行,第三个方法必须在第二个方法之后执行;

在这里插入图片描述

  1. 依赖关系可以通过并发机制实现。使用一个共享变量 firstJobDone 协调第一个方法与第二个方法的执行顺序,使用另一个共享变量 secondJobDone 协调第二个方法与第三个方法的执行顺序。

算法

  1. 首先初始化共享变量 firstJobDone 和 secondJobDone,初始值表示所有方法未执行;
  2. 方法 first() 没有依赖关系,可以直接执行。在方法最后更新变量 firstJobDone 表示该方法执行完成;
  3. 方法 second() 中,检查 firstJobDone 的状态。如果未更新则进入等待状态,否则执行方法second()。在方法末尾,更新变量secondJobDone 表示方法 second() 执行完成;
  4. 方法 third() 中,检查 secondJobDone 的状态。与方法 second() 类似,执行 third() 之前,需要先等待 secondJobDone 的状态;

在这里插入图片描述

代码

class Foo {

  private AtomicInteger firstJobDone = new AtomicInteger(0);
  private AtomicInteger secondJobDone = new AtomicInteger(0);

  public Foo() {}

  public void first(Runnable printFirst) throws InterruptedException {
    // printFirst.run() outputs "first".
    printFirst.run();
    // mark the first job as done, by increasing its count.
    firstJobDone.incrementAndGet();
  }

  public void second(Runnable printSecond) throws InterruptedException {
    while (firstJobDone.get() != 1);
    // printSecond.run() outputs "second".
    printSecond.run();
    // mark the second as done, by increasing its count.
    secondJobDone.incrementAndGet();
  }

  public void third(Runnable printThird) throws InterruptedException {
    while (secondJobDone.get() != 1);
    // printThird.run() outputs "third".
    printThird.run();
  }
}

2.2 解法2:信号量

代码

class Foo {
    private Semaphore second = new Semaphore(0);
    private Semaphore third = new Semaphore(0);

    public Foo() {}

    public void first(Runnable printFirst) throws InterruptedException {
        printFirst.run();
        second.release();
    }

    public void second(Runnable printSecond) throws InterruptedException {
        second.acquire();
        printSecond.run();
        third.release();
    }

    public void third(Runnable printThird) throws InterruptedException {
        third.acquire();
        printThird.run();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hellosc01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值