一、前置知识
线程间通信三要素:
多线程+判断+操作+通知+资源类。
上面的五个要素,其他三个要素就是普通的多线程程序问题,那么通信就需要线程间的互相通知,往往伴随着何时通信的判断逻辑。
在 java 的 Object 类里就提供了对应的方法来进行通知,同样的,保证安全的判断采用隐式的对象锁,也就是 synchronized 关键字实现。这块内容在:
java多线程:线程间通信——生产者消费者模型
已经写过。
2|0二、使用 Lock 实现线程间通信
那么,我们知道 juc 包里提供了显式的锁,即 Lock 接口的各种实现类,如果想用显式的锁来实现线程间通信问题,唤醒方法就要使用对应的 Conditon 类的 await 和 signalAll 方法。(这两个方法名字也能看得出来,对应 Object 类的 wait 和 notifyAll)
Condition 对象的获取可以通过具体的 Lock 实现类的对象的 newCondition 方法获得。
public class Communication2 {
public static void main(String[] args) {
Container container = new Container();
new Thread(()->{
for (int i = 0; i < 10; i++){
container.increment();
}
},"生产者1").start();
new Thread(()->{
for (int i = 0; i < 10; i++){
container.decrenment();
}
},"消费者1").start();
new Thread(()->{
for (int i = 0; i < 10; i++){
container.increment();
}
},"生产者2").start();
new Thread(()->{
for (int i = 0; i < 10; i++){
container.decrenment();
}
},"消费者2").start();
new Thread(()->{
for (int i = 0; i < 10; i++){
container.increment();
}
},"生产者3").start();
new Thread(()->{
for (int i = 0; i < 10; i++){
container.decrenment();
}
},"消费者3").start();
}
}
class Container{
private int count = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment(){
lock.lock();
try {
while (count != 0){
condition.await();
}
count++;
System.out.println(Thread.currentThread().getName() + " "+count);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrenment(){
lock.lock();
try{
while (count == 0){
condition.await();
}
count--;
System.out.println(Thread.currentThread().getName()+ " " + count);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
输出也没有任何问题。
这里面我们模拟了 3 个生产者和 3 个消费者,进行对一个资源类,其实就是一个数字 count 的操作,并使用 Condition 类来进行唤醒操作。
从目前代码的用法来看, juc 包的 Lock 接口实现类和之前使用 synchronized + Object 类的线程通信方法是一样的,但是并发包的开发工具给了他更多的灵活性。灵活在哪?
三、唤醒特定线程
新技术解决了旧问题,这个方法解决的问题就是,在生产者消费者问题里:有时候我们并不想唤醒所有的对面伙伴,而只想要唤醒特定的一部分,这时候该怎么办呢?
如果没有显式的 lock,我们的思路可能是:
- 采用一个标志对象,可以是一个数值或者别的;
- 当通信调用 signalAll 的时候,其他线程都去判断这个标志,从而决定自己应不应该工作。
这种实现是可行的,但是本质上其他线程都被唤醒,然后一直阻塞+判断,其实还是在竞争。那么 Condition 类其实就已经提供了对应的方法,来完成这样的操作:
我们看这样一个需求:
- 同样是多线程操作、需要通信。但是我们要指定各个线程交替的顺序,以及指定唤醒的时候是指定哪个具体线程,这样就不会存在唤醒所有线程然后他们之间互相竞争了。
- 具体说是:AA 打印 5 次,BB 打印 10 次, CC 打印 15次,然后接着从 AA 开始一轮三个交替。
代码如下:
public class TakeTurnPrint {
public static void main(String[] args) {
ShareResource resource = new ShareResource();
new Thread(()->{resource.printA();},"A").start();
new Thread(()->{resource.printB();},"B").start();
new Thread(()->{resource.printC();},"C").start();
}
}
/**
* 资源
*/
class ShareResource{
private int signal = 0;//0-A,1-B,2-C
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
public void printA(){
lock.lock();
try{
while (signal != 0){
condition.await();//精准
}
for (int i = 0; i < 5; i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
signal = 1;//精准
condition1.signal();//精准指定下一个
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try{
while (signal != 1){
condition1.await();//精准
}
for (int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
signal = 2;//精准
condition2.signal();//精准指定下一个
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try{
while (signal != 2){
condition2.await();//精准
}
for (int i = 0; i < 15; i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
signal = 0;//精准
condition.signal();//精准指定下一个
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
其中,使用三个 Condition 对象,用一个 signal 的不同值,来通知不同的线程。