Condition 可以指定唤醒线程?
先说说问题的起源,最近在网上看到一个网友提问,说是看到了一个教学视频,讲到了线程的执行顺序问题,有一个疑惑,先将代码贴出来。
/**
* @describe: 测试
* @author: sunlight
* @date: 2021/7/31 11:24
*/
public class Test {
public static void main(String[] args) {
Loop loop = new Loop();
new Thread(() -> {
loop.loopA();
}, "a").start();
new Thread(() -> {
loop.loopB();
}, "b").start();
new Thread(() -> {
loop.loopC();
}, "c").start();
}
}
/**
* 定义资源类
*/
class Loop {
/**
* 线程执行顺序标记,1:表示loopA执行,2:表示loopB执行,3:表示loopC执行
*/
private volatile int number = 1;
/**
* 获得lock锁
*/
private Lock lock = new ReentrantLock();
/**
* 创建condition对象用来await(阻塞)和signal(唤醒)线程A
*/
private Condition c1 = lock.newCondition();
/**
* 创建condition对象用来await(阻塞)和signal(唤醒)线程B
*/
private Condition c2 = lock.newCondition();
/**
* 创建condition对象用来await(阻塞)和signal(唤醒)线程C
*/
private Condition c3 = lock.newCondition();
protected void loopA() {
//上锁
lock.lock();
try {
//如果不是第一个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
while (number != 1) {
try {
//阻塞类似wait()
c1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":A");
//将标记改成2
number = 2;
//唤醒第二个线程,类似notify()方法
c2.signal();
} finally {
lock.unlock();//解锁
}
}
protected void loopB() {
//上锁
lock.lock();
try {
//如果不是第二个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
while (number != 2) {
try {
//阻塞类似wait()
c2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":B");
//将标记改成3
number = 3;
//唤醒第三个线程,类似notify()方法
c3.signal();
} finally {
lock.unlock();//解锁
}
}
protected void loopC() {
//上锁
lock.lock();
try {
//如果不是第二个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
while (number != 3) {
try {
//阻塞类似wait()
c3.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":C");
//将标记改成1
number = 1;
//唤醒第一个线程,类似notify()方法
c1.signal();
} finally {
lock.unlock();//解锁
}
}
}
毫无疑问,程序的执行结果肯定是,不管重复多少次也是一样的。
a:A
b:B
c:C
网友的疑惑点在于以下代码:
//唤醒第二个线程,类似notify()方法
c2.signal();
为什么 c2.signal();可以唤醒第二个线程呢?线程和Condition是如何绑定的呢?
问题分析
要回答这个问题,首先要确定两个条件:
- condition是否有控制指定线程的能力?
- condition是什么时候和指定线程绑定的?
首先看第一个问题,condition有控制指定线程的能力吗?答案显然是否定的,因为线程的执行是由操作系统来调度的,程序无法指定线程的执行。再看第二个问题,condition有和线程绑定吗?答案也是否定的,从整个程序来看,condition对象和线程之间唯一的关系就是condition的数字和线程的字母具有连贯性,我们假设condition有唤醒指定线程的能力,那c2为什么是唤醒线程B,就因为一个是数字2,一个是字母2?从java序号都是从0开始来看,不应该是c0唤醒线程a,c1唤醒线程b吗,在我们没有定义c0的情况下,岂不是要空指针?
问题验证
为了验证上面的猜想,我们先对程序做一个修改,就是将condition条件改成一个,其他地方不变,如下所示:
/**
* @describe: 测试只有一个条件
* @author: sunlight
* @date: 2021/7/31 11:24
*/
public class Test2 {
public static void main(String[] args) {
Loop2 loop = new Loop2();
new Thread(() -> {
loop.loopA();
}, "a").start();
new Thread(() -> {
loop.loopB();
}, "b").start();
new Thread(() -> {
loop.loopC();
}, "c").start();
}
}
/**
* 定义资源类
*/
class Loop2 {
/**
* 线程执行顺序标记,1:表示loopA执行,2:表示loopB执行,3:表示loopC执行
*/
private volatile int number = 1;
/**
* 获得lock锁
*/
private Lock lock = new ReentrantLock();
/**
* 创建condition对象用来await(阻塞)和signal(唤醒)线程A
*/
private Condition c = lock.newCondition();
protected void loopA() {
//上锁
lock.lock();
try {
//如果不是第一个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
while (number != 1) {
try {
//阻塞类似wait()
c.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":A");
//将标记改成2
number = 2;
//唤醒第二个线程,类似notify()方法
c.signal();
} finally {
lock.unlock();//解锁
}
}
protected void loopB() {
//上锁
lock.lock();
try {
//如果不是第二个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
while (number != 2) {
try {
//阻塞类似wait()
c.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":B");
//将标记改成3
number = 3;
//唤醒第三个线程,类似notify()方法
c.signal();
} finally {
lock.unlock();//解锁
}
}
protected void loopC() {
//上锁
lock.lock();
try {
//如果不是第二个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
while (number != 3) {
try {
//阻塞类似wait()
c.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":C");
//将标记改成1
number = 1;
//唤醒第一个线程,类似notify()方法
c.signal();
} finally {
lock.unlock();//解锁
}
}
}
显然,执行结果依然是正确的。
然后我们再来做第二个验证,我们还是拿第一次的代码,我们调整一下线程的启动顺序,且做一个打印,代码如下:
/**
* @describe: 测试
* @author: sunlight
* @date: 2021/7/31 11:24
*/
public class Test {
public static void main(String[] args) {
Loop loop = new Loop();
new Thread(() -> {
loop.loopB();
}, "b").start();
new Thread(() -> {
loop.loopA();
}, "a").start();
new Thread(() -> {
loop.loopC();
}, "c").start();
}
}
/**
* 定义资源类
*/
class Loop {
/**
* 线程执行顺序标记,1:表示loopA执行,2:表示loopB执行,3:表示loopC执行
*/
private volatile int number = 1;
/**
* 获得lock锁
*/
private Lock lock = new ReentrantLock();
/**
* 创建condition对象用来await(阻塞)和signal(唤醒)线程A
*/
private Condition c1 = lock.newCondition();
/**
* 创建condition对象用来await(阻塞)和signal(唤醒)线程B
*/
private Condition c2 = lock.newCondition();
/**
* 创建condition对象用来await(阻塞)和signal(唤醒)线程C
*/
private Condition c3 = lock.newCondition();
protected void loopA() {
//上锁
lock.lock();
try {
System.out.println("我是线程a,当前标记为:" + number);
//如果不是第一个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
while (number != 1) {
try {
//阻塞类似wait()
c1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":A");
//将标记改成2
number = 2;
//唤醒第二个线程,类似notify()方法
c2.signal();
} finally {
lock.unlock();//解锁
}
}
protected void loopB() {
//上锁
lock.lock();
try {
System.out.println("我是线程b,当前标记为:" + number);
//如果不是第二个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
while (number != 2) {
try {
//阻塞类似wait()
c2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":B");
//将标记改成3
number = 3;
//唤醒第三个线程,类似notify()方法
c3.signal();
} finally {
lock.unlock();//解锁
}
}
protected void loopC() {
//上锁
lock.lock();
try {
System.out.println("我是线程c,当前标记为:" + number);
//如果不是第二个标志位,就阻塞,为了解决虚假唤醒问题,使用while关键字
while (number != 3) {
try {
//阻塞类似wait()
c3.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":C");
//将标记改成1
number = 1;
//唤醒第一个线程,类似notify()方法
c1.signal();
} finally {
lock.unlock();//解锁
}
}
}
查看执行结果:
我是线程b,当前标记为:1
我是线程a,当前标记为:1
a:A
我是线程c,当前标记为:2
b:B
c:C
显然,先是执行了线程b,然后被等待,然后唤醒了a且执行完毕,然后被唤醒的是线程c,又被等待,然后是线程b被唤醒继续执行,执行完后唤醒其他线程,再然后c被唤醒再次执行,程序结束。由此可见,condition唤醒的线程完全具有随机性,也就是说,整个程序中,控制顺序的其实是标记number,condition只是起到了阻塞和唤醒线程的作用,没有控制顺序的作用。
网上或者教学资料为什么这么写?
个人觉得,两个原因:
- 真正懂condition作用的,之所以要设置三个变量c1c2和c3,是为了让程序看起来更直观,更便于阅读,第二个测试代码显然没有第一个源代码阅读性强。
- 没有真正懂condition的作用,只是看jdk文档中demo用了两个condition,就错误的以为condition可以指定线程,没有深入的思考,其实问题分析中的第二个点很容易就想到condition不可能和线程顺序有关系的。