问题LeetCode 1115:https://leetcode-cn.com/problems/print-foobar-alternately/
这次使用monitor和ReentrantLock两种锁实现一下。
先实现一个利用内置锁协调两个线程的代码。通过synchronized关键字通过内部锁进行线程协调,实际上帮助我们隐藏了“条件队列”这样一个概念。粗浅地讲,内部锁就是一个可重入的互斥锁。下面的代码是利用内部锁的一个实现。
知识点细节见注释:
public class FooBarByMonitor {
private int n;
public FooBarByMonitor(int n) {
this.n = n;
}
private final Object lock = new Object(); //_ 因为不能修改题目的方法前面,所以不能使用默认当前对象的monitor。创建一个对象利用它的monitor。
private volatile boolean isFooTurn = true; //_ 定义一个boolean变量表示互斥的2种状态。
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
synchronized (lock) {
while (!isFooTurn) lock.wait(); //_ 如果不该打印foo则阻塞(把当前线程放到lock的等待队列),等待通知。第一次是可打印的。
printFoo.run();
isFooTurn = false; //_ 更改到可以打印bar的状态。
lock.notify(); //_ 因为只有两个线程,所以notify是可以的,否则应使用notifyAll。
}
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
synchronized (lock) {
while (isFooTurn) lock.wait(); //_ 如果不该打印bar则阻塞,等待通知。
printBar.run();
isFooTurn = true; //_ 更改到可以打印foo的状态。
lock.notify(); //_ 唤醒其它等待的线程。
}
}
}
}
前面文章《条件队列是个线程的队列》里提到Lock是内部锁的显示化,Condition是内部锁条件队列的显示化。对比下面和上面的代码,应该可以体会到这一点。通过由Lock对象针对不同"条件谓词"定义出不同等待队列,可以做得比使用内部锁提供的多个条件谓词共享一个等待队列的模式更高效地进行线程协调。所谓线程协调就是安排线程适当的阻塞、唤醒、运行的切换而已。
最开始学习“条件队列”时,我对“条件”这个词感到莫名其妙。为什么不叫‘等待队列’?‘条件’从何而来?
其实这要从 谓词——Predicate 说起,可参考wiki定义。简单的说“谓词”就是指那些返回真或假的表达式。而条件——Condition就是某事成真或假的前提。我们常用变量表示conditon的状态,这就是condition variable。
条件谓词——Conditon Predicate,翻译成人话就是最终用一个变量得出真、假的判断。
那这跟多线程编程又有什么关系呢?关系还挺深的。并发编程的核心是协调线程的运行,就是有时候一些线程可以运行而另一些线程要暂停下来。那么根据什么来阻塞、唤醒线程呢?这就要根据你实际业务里的逻辑来决定,这个逻辑运算的结果就是个boolean值。这个逻辑最终体现在condition variable、体现在Conditon Predicate上。对于下面这段轮流打印foobar的代码,决定线程阻塞还是运行的条件就是“该打印foo了吗?”,我们用isFooTurn这个变量来表示。对于阻塞队列的添加、删除操作来说,对应的条件变量就应该能反应这个队列的满、空状态。多线程的协调就是合理的基于条件变量改变线程自身的状态以及改变条件变量的状态来完成的。
回到“条件队列”这词上来就容易了,条件队列里装的都是等待条件发生变化的线程。希望到此你跟我一样对“条件队列”这个词再没有违和感了。:)
知识点细节见注释:
public class FooBarByLock {
private int n;
public FooBarByLock(int n) {
this.n = n;
}
//_ 这里相对使用Semaphore的版本多出一个状态变量表示 应该打印foo还是bar。
//_ 在Semaphore的版本里我们没有多定义一个变量是因为 对foo信号量初始化为1,即实现了先打印1。
//_ 另外由于信号量交替acquire,release的特性保证了不需要额外定义一个状态变量。
//_ 同时,这个变量也是用于表达下面两个条件队列对应'条件谓词'的关键。
private volatile boolean isFooTurn = true;
private final Lock lock = new ReentrantLock();
private final Condition fooCondition = lock.newCondition(); //_ 对应条件谓词 'isFooTurn == true'
private final Condition barCondition = lock.newCondition(); //_ 对应条件谓词 'isFooTurn == false'
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
lock.lock();
try {
//_ 虽然在这道例题里可以使用if,但是如果有多个线程可以更改这个用于表达条件谓词的变量isFooTurn就会出现问题。
while (!isFooTurn) { //_ 虽然使用针对此题是没问题的,作为定式这里直接使用循环。
fooCondition.await(); //_ 等待打印foo的条件谓词成立。因为isFooTurn初始化为true,所以第一轮不阻塞。
}
printFoo.run();
isFooTurn = false; //_ 更改条件谓词,使另个线程可以继续。
barCondition.signal(); //_ 通知bar的条件队列,可以打印了。
} finally {
lock.unlock();
}
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
lock.lock();
try {
while (isFooTurn) {
barCondition.await(); //_ 阻塞。等待打印bar的条件谓词'isFooTurn == false'成立
}
printBar.run();
isFooTurn = true;
fooCondition.signal();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
FooBarByLock foobar = new FooBarByLock(20);
new Thread(() -> {
try {
foobar.foo(() -> System.out.print("foo"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
foobar.bar(() -> System.out.print("bar"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
Reference: https://web.stanford.edu/~ouster/cgi-bin/cs140-spring14/lecture.php?topic=locks
http://pages.cs.wisc.edu/~remzi/OSTEP/threads-cv.pdf
https://en.wikipedia.org/wiki/Predicate_(mathematical_logic)