@[TOC](算法笔记-lc-1114. 按序打印(多线程简单题))
题目
题干
给你一个类:
public class Foo {
public void first() { print(“first”); }
public void second() { print(“second”); }
public void third() { print(“third”); }
}
三个不同的线程 A、B、C 将会共用一个 Foo 实例。
线程 A 将会调用 first() 方法
线程 B 将会调用 second() 方法
线程 C 将会调用 third() 方法
请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。
提示:
尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。
你看到的输入格式主要是为了确保测试的全面性。
示例
示例 1:
输入:nums = [1,2,3]
输出:“firstsecondthird”
解释:
有三个线程会被异步启动。输入 [1,2,3] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 second() 方法,线程 C 将会调用 third() 方法。正确的输出是 “firstsecondthird”。
示例 2:
输入:nums = [1,3,2]
输出:“firstsecondthird”
解释:
输入 [1,3,2] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 third() 方法,线程 C 将会调用 second() 方法。正确的输出是 “firstsecondthird”。
提示:
nums 是 [1, 2, 3] 的一组排列
题解
并发问题
并发问题来自并发计算的场景,该场景下,程序在多线程(或多进程)中 同时 执行。
同时进行并不是完全指进程或线程在不同的物理 CPU 上独立运行,更多情况下,是在一个物理 CPU 上交替执行多个线程或进程。并发既可在线程中,也可在进程中。
并发主要为多任务情况设计。但如果应用不当,可能会引发一些漏洞。按照情况不同,可以分为三种:
竞态条件:由于多进程之间的竞争执行,导致程序未按照期望的顺序输出。
死锁:并发程序等待一些必要资源,导致没有程序可以执行。
资源不足:进程被永久剥夺了运行所需的资源。
此题中存在竞态条件。下面展示一个竞态条件的例子。
假设有一个方法 withdraw(amount),如果请求量小于当前余额,则从当前余额中减去请求量,然后返回余额。方法定义如下:
int balance = 500;
int withdraw(int amount) {
if (amount < balance) {
balance -= amount;
}
return balance;
}
我们 期望 该方法执行后余额永远不会为负。
但是有可能出现竞态条件,使得余额变为负数。假设两个线程同时使用不同的参数执行该方法。例如:线程 1 执行 withdraw(amount=400),线程 2 执行 withdraw(amount=200)。这两个线程的执行顺序如下图所示。在每个时刻只执行一条语句。
上述流程执行结束后,余额变成负数,这并不是期望的输出。
无竞争并发
并发问题有一个共同特征:多个线程/进程之间共享一些资源(例如:余额)。由于无法消除资源共享的约束,防止并发问题就变成了 资源共享的协调 问题。
根据这个思路,如果可以确保程序中 关键部分代码的独占性(例如:检查和减少余额),就可以防止程序进入不一致的状态。
竞争条件的解决方案为:需要某些关键部分代码具有排他性,即在给定的时间内,只有一个线程可以进入关键部分代码。
可以将这种机制看做限制关键部分代码访问的锁。在前面示例的关键部分代码加锁,即检查余额和减少余额的语句。然后重新运行两个线程,会有下图的执行顺序:
在该机制下,一旦一个线程进入关键部分,它就可以阻止其他线程进入该关键部分。例如,在时间点 3,线程 2 进入关键部分,那么在时间点 4,如果没有锁保护,线程 1 就可能进入关键部分。最后两个线程同时运行,保证系统的一致性,并确保余额正确。
如果该线程未被授权进入关键代码,可以认为该线程被阻塞或进入睡眠状态。例如,线程 1 在时间点 4 被阻塞,之后关键部分被释放,可以通知其他等待线程。线程 2 在时间点 5 释放了关键部分,就可以通知 线程 1 进入。
这种机制还具有唤醒其他等待线程的功能。
总之,为了防止出现并发竞争状态,需要一种具有两种功能的机制:1)关键部分的访问控制;2)通知阻塞线程。
方法一:使用 synchronization
思路
题目要求按顺序依次执行三个方法,且每个方法都在单独的线程中运行。为了保证线程的执行顺序,可以在方法之间创建一些依赖关系,即第二个方法必须在第一个方法之后执行,第三个方法必须在第二个方法之后执行。
方法对之间的依赖关系形成了所有方法的特定的执行顺序。例如 A < B, B < C,则所有方法的执行顺序为 A < B < C。
依赖关系可以通过并发机制实现。使用一个共享变量 firstJobDone 协调第一个方法与第二个方法的执行顺序,使用另一个共享变量 secondJobDone 协调第二个方法与第三个方法的执行顺序。
算法
首先初始化共享变量 firstJobDone 和 secondJobDone,初始值表示所有方法未执行。
方法 first() 没有依赖关系,可以直接执行。在方法最后更新变量 firstJobDone 表示该方法执行完成。
方法 second() 中,检查 firstJobDone 的状态。如果未更新则进入等待状态,否则执行方法 second()。在方法末尾,更新变量 secondJobDone 表示方法 second() 执行完成。
方法 third() 中,检查 secondJobDone 的状态。与方法 second() 类似,执行 third() 之前,需要先等待 secondJobDone 的状态。
实现
上述算法的实现在很大程度上取决于选择的编程语言。尽管在 Java,C++ 和 Python 中都存在互斥与信号量,但不同语言对并发机制有不同实现。
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) {
// waiting for the first job to be done.
}
// 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) {
// waiting for the second job to be done.
}
// printThird.run() outputs "third".
printThird.run();
}
}
方法二:信号量机制
private Semaphore two = new Semaphore(0);
private Semaphore three = 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.release();
}
public void second(Runnable printSecond) throws InterruptedException {
two.acquire();
// printSecond.run() outputs "second". Do not change or remove this line.
printSecond.run();
three.release();
}
public void third(Runnable printThird) throws InterruptedException {
three.acquire();
// printThird.run() outputs "third". Do not change or remove this line.
printThird.run();
}
方法三:wait()+notifyAll()
class Foo {
private boolean firstFinished;
private boolean secondFinished;
private Object lock = new Object();
public Foo() {
}
public void first(Runnable printFirst) throws InterruptedException {
synchronized (lock) {
// printFirst.run() outputs "first". Do not change or remove this line.
printFirst.run();
firstFinished = true;
lock.notifyAll();
}
}
public void second(Runnable printSecond) throws InterruptedException {
synchronized (lock) {
while (!firstFinished) {
lock.wait();
}
// printSecond.run() outputs "second". Do not change or remove this line.
printSecond.run();
secondFinished = true;
lock.notifyAll();
}
}
public void third(Runnable printThird) throws InterruptedException {
synchronized (lock) {
while (!secondFinished) {
lock.wait();
}
// printThird.run() outputs "third". Do not change or remove this line.
printThird.run();
}
}
}
方法四:sleep(0)
class Foo {
int flag=1;
public Foo() {
}
public void first(Runnable printFirst) throws InterruptedException {
// printFirst.run() outputs "first". Do not change or remove this line.
printFirst.run();
flag=2;
}
public void second(Runnable printSecond) throws InterruptedException {
while(true){
if(flag>=2){
break;
}
else{
Thread.sleep(0);
}
}
// printSecond.run() outputs "second". Do not change or remove this line.
printSecond.run();
flag=3;
}
public void third(Runnable printThird) throws InterruptedException {
while(true){
if(flag>=3){
break;
}
else{
Thread.sleep(0);
}
}
// printSecond.run() outputs "second". Do not change or remove this line.
printThird.run();
}
}