目录
最近做了一道多线程同步的题目,我使用了条件锁的方式解答。通过做这道题,我们能对锁的应用有一个基本的了解,这篇文章就来简单的讲解一下。
Ps:做完了后发现这是力扣上的原题,题目链接:https://leetcode-cn.com/problems/print-zero-even-odd/,这是我的提交记录:
可以在力扣网上看到这道题多种多样的解法,请读者自行探索,本文只针对条件锁实现方式讲解。
题目:
假设有这么一个类:
class ZeroEvenOdd {
public ZeroEvenOdd(int n) { ... } // 构造函数
public void zero(printNumber) { ... } // 仅打印出 0
public void even(printNumber) { ... } // 仅打印出 偶数
public void odd(printNumber) { ... } // 仅打印出 奇数
}
相同的一个 ZeroEvenOdd 类实例将会传递给三个不同的线程:
线程 A 将调用 zero(),它只输出 0 。
线程 B 将调用 even(),它只输出偶数。
线程 C 将调用 odd(),它只输出奇数。
每个线程都有一个 printNumber 方法来输出一个整数。请修改给出的代码以输出整数序列 010203040506... ,其中序列的长度必须为 2n。
示例 1:
输入:n = 2
输出:"0102"
说明:三条线程异步执行,其中一个调用 zero(),另一个线程调用 even(),最后一个线程调用odd()。正确的输出为 "0102"。
示例 2:
输入:n = 5
输出:"0102030405"
解答:
package com.demo;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntConsumer;
public class ZeroEvenOddImpl1 {
private int n;
private int curr = 0;
private ReentrantLock lock;
private Condition cA;
private Condition cB;
private Condition cC;
private volatile int control = 0;
public ZeroEvenOddImpl1(int n) {
lock = new ReentrantLock();
cA = lock.newCondition();
cB = lock.newCondition();
cC = lock.newCondition();
this.n = n;
}
public static void main(String[] args) {
ZeroEvenOddImpl1 impl = new ZeroEvenOddImpl1(10);
Thread tA = new Thread( ()-> {
try {
impl.zero(i -> System.out.print(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread tB = new Thread( ()-> {
try {
impl.even(i -> System.out.print(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread tC = new Thread( ()-> {
try {
impl.odd(i -> System.out.print(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
tC.start();
tB.start();
tA.start();
}
// printNumber.accept(x) outputs "x", where x is an integer.
public void zero(IntConsumer printNumber) throws InterruptedException {
while(control<2);
lock.lock();
try {
while(true) {
if(curr >=n) {
cB.signal();
cC.signal();
return;
}
printNumber.accept(0);
cC.signal();
cA.await();
if(curr >=n) {
cB.signal();
cC.signal();
return;
}
printNumber.accept(0);
cB.signal();
cA.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
} finally {
lock.unlock();
}
}
public void odd(IntConsumer printNumber) throws InterruptedException {
lock.lock();
control++;
try {
while (true) {
cC.await();
if(curr >=n) {
cA.signal();
return;
}
printNumber.accept(++curr);
cA.signal();
}
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
} finally {
lock.unlock();
}
}
public void even(IntConsumer printNumber) throws InterruptedException {
lock.lock();
control++;
try {
while(true) {
cB.await();
if(curr >=n) {
cA.signal();
return;
}
printNumber.accept(++curr);
cA.signal();
}
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
} finally {
lock.unlock();
}
}
}
讲解:
这里先讲解代码中使用的api,以下是条件锁创建的方式,一个ReentrantLock可以创建多个条件锁。
ReentrantLock lock = new ReentrantLock();
Condition cA = lock.newCondition();
本次代码使用了Condition的两个方法:await、signal。await的作用是阻塞当前线程,并释放掉锁资源。signal的作用是唤醒调用了await的线程,被唤醒线程从调用await处继续执行。这两个函数都需要在获取锁后调用,否则抛出IllegalMonitorStateException异常,即需要先执行lock.lock()(养成良好的编码习惯,请在finally 中释放锁资源: lock.unlock())。
接下来讲解解题思路,这道题本质上就是在控制多个线程的执行顺序(即同步)。我们把题目中有几个线程,线程之间又是什么执行顺序的细节忽略掉,只关心控制的方法。这里的思路就是为每一个线程分配一个单独的条件锁。假设为B线程分配的条件锁为cB,那么控制B线程的同步需要先执行cB.await()来阻塞自己,并等待其他线程通知自己执行任务。如果A线程执行完毕后轮到B线程执行,那么A线程在执行完任务后去调用cB.signal()将B线程唤醒。同理,如果B线程执行完毕后轮到C线程执行,那么也在自己执行完任务后去调用cC.signal()将C线程唤醒,通知C线程执行任务。解题思路有了,再解题很容易了。不管题目中有几个线程,它们之间又是什么执行顺序,只要把这个思路往上套都能解决问题。
最后再来讲解一下本题代码中的两个小细节。一:细心的读者已经在思考control变量的作用了,其实control变量是为了保证A线程要在B、C线程获取锁之后再获取锁。为什么要这样做呢,假设一种情况:A线程最先获取到锁,那么A线程会执行cC.signal(),而此时C线程还没有获取到锁,cC.signal()将起不到任何作用,这就会导致C锁永远得不到通知,从而导致线程间产生死锁问题,任务全部卡住不动。而A线程获取锁的顺序是第二的话也会有类似的问题。有的人可能会想通过延时启动或者通过使用Thread类的isAlive方法来达到让A最后获取锁的效果,但是这样都不能百分百保证达到目的,是不安全的代码。二:如果n为一个偶数,然后将even方法finally代码块中的释放锁代码注释掉,那么在执行后会发现程序不能退出了,这是因为虽然B线程执行完操作已经通知A线程了,但是通知后自己的线程就通知了,这时如果不释放锁是不能真的通知到的。所以我们一定要养成好的释放资源习惯。
最后再提供一种该题的简单是实现方式,但是该实现方式应该是多线程不安全的,在牛客网提交也提示超时,欢迎有想法的读者评论讨论。
package com.demo;
import java.util.Currency;
import java.util.function.IntConsumer;
public class ZeroEvenOddImpl2 {
private int n;
private volatile int exec = 0;
private volatile int curr = 0;
public ZeroEvenOddImpl2(int n) {
this.n = n;
}
public static void main(String[] args) {
ZeroEvenOddImpl2 impl = new ZeroEvenOddImpl2(51);
Thread tA = new Thread( ()-> {
try {
impl.zero(i -> System.out.print(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread tB = new Thread( ()-> {
try {
impl.even(i -> System.out.print(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread tC = new Thread( ()-> {
try {
impl.odd(i -> System.out.print(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
tC.start();
tB.start();
tA.start();
}
// printNumber.accept(x) outputs "x", where x is an integer.
public void zero(IntConsumer printNumber) throws InterruptedException {
while(curr < n) {
while(exec!=0 && curr < n);
if(curr < n) {
printNumber.accept(0);
exec = 1;
}
while(exec!=0 && curr < n);
if(curr < n) {
printNumber.accept(0);
exec = 2;
}
}
}
public void even(IntConsumer printNumber) throws InterruptedException {
while(curr<n) {
while(exec!=2 && curr<n);
if(curr<n) {
printNumber.accept(++curr);
exec = 0;
}
}
}
public void odd(IntConsumer printNumber) throws InterruptedException {
while(curr<n) {
while(exec!=1 && curr<n);
if(curr<n) {
printNumber.accept(++curr);
exec = 0;
}
}
}
}