线程间的通信:主要是多个线程间有依赖,需要进行消息的沟通,例如:搬运工搬运10号货物,
需要9号货物先搬走, 10号货物才能搬走,因此这两个线程需要进行通信,告知情况。
1 线程间通信
线程间的通信有:1 共享变量;2 wait、notify/notifyAll;3 lock、condition;4 共享管道。
(1) wait、notify/notifyAll
通过Object对象中的wait(当前线程等待)和notify(通知其他线程)方法,我们就可以建立一个多线程之间的通信。
public class MyThread {
//公共变量,用来标明通信
public static Integer num = 0;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
if (num != 9) {
try {
System.out.println("9号物品还没有搬走,我等等");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName()+":我搬走了10号物品");
}
}
},"10号工人").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":我搬走9号物品了。");
num = 9;
this.notifyAll();
}
},"9号工人").start();
}
}
-- 执行结果:
Exception in thread "10号工人" java.lang.IllegalMonitorStateException
发现上述执行结果报错,一脸的懵逼,查看IllegalMonitorStateException:Thrown to indicate that a thread has attempted to wait on an object’s monitor or to notify other threads waiting on an object’s monitor without owning the specified monitor。当前线程试图在一个对象监视器上等待或者试图唤醒在该对象监视器上的其他等待线程时,发现自己竟然没有拥有这个监视器,所以就会抛出这个异常,那如何拥有对象的监视器呢?
通过搜索发现,获取对象的监视器有3中方式:
- 通过执行此对象的同步 (Sychronized) 实例方法。
- 过执行在此对象上进行同步的 synchronized 语句的正文。
- 对于 Class 类型的对象,可以通过执行该类的同步静态方法。
通过上述方法,发现获取对象的监视器需要通过synchronized来实现。
获取对象监视器
进阶代码二:
public class MyThread {
//公共变量,用来标明通信
public static Integer num = 0;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
if (num != 9) {
synchronized(this){
try {
System.out.println("9号物品还没有搬走,我等等");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else {
System.out.println(Thread.currentThread().getName()+":我搬走了10号物品");
}
}
},"10号工人").start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (this){
System.out.println(Thread.currentThread().getName() + ":我搬走9号物品了。");
num = 9;
this.notifyAll();
}
}
},"9号工人").start();
}
}
--- 输出内容:
9号物品还没有搬走,我等等
9号工人:我搬走9号物品了。
不报错,9号工人已经搬走货物了,等了好久还没有等到10号工人搬东西,后来发现程序一致运行,但是10号工人阻塞了。
排查发现,索然大家都获取到了对象的监视器,但是发现对象不一样啊,这个this和那个this不是同一个对象。
同一对象的监视器
进阶代码三,既然没有获取同一个对象的监视器,那我们就用同一个对象,正好num是公共的,可以大家一同使用。
public static Integer num = 0;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
if (num != 9) {
synchronized(num){
try {
System.out.println("9号物品还没有搬走,我等等");
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else {
System.out.println(Thread.currentThread().getName()+":我搬走了10号物品");
}
}
},"10号工人").start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (num){
System.out.println(Thread.currentThread().getName() + ":我搬走9号物品了。");
num = 9;
num.notifyAll();
}
}
},"9号工人").start();
}
---输出
Exception in thread "9号工人" java.lang.IllegalMonitorStateException
结果还是报上一个错。
通过上述报错,我们发现9号线程报错了,说明synchronized获取的监视器对象和num.notifyAll()不是同一个对象,那这是什么地方出现问题?
通过代码我们发现9号线程进行赋值,是不是赋值导致对象发生变化?通过9号线程的debug发现,num通过赋值,对象时不一样的。因此当我们通过wait和notify进行线程通信时,必须要保证对象唯一,最好不要用有意义的对象,直接用一个无关的唯一的,这就是我们在很多方法中,看到private final Object lock这种代码,直接作为synchronized的对象,就是为了防止出现赋值或其他操作导致对象发生变化。
if和while
通过上述的分析,进阶代码4
public class MyThread {
//公共变量,用来标明通信
public static Integer num = 0;
public static final Object obj = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
if (num != 9) {
synchronized(obj){
try {
System.out.println(Thread.currentThread().getName()+":9号物品还没有搬走,我等等");
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else {
System.out.println(Thread.currentThread().getName()+":我搬走了10号物品");
}
System.out.println(Thread.currentThread().getName()+":我结束了");
}
},"10号工人").start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj){
System.out.println(Thread.currentThread().getName() + ":我搬走9号物品了。");
num = 9;
obj.notifyAll();
}
}
},"9号工人").start();
}
}
---
10号工人:9号物品还没有搬走,我等等
9号工人:我搬走9号物品了。
10号工人:我结束了
通过输出内容,发现10号工人就没有搬东西,被唤醒后,直接就结束了,
这就是if原因,判断后就不会再次判断了。
因此需要通过while进行处理,即:线程被唤醒后也需要再次判断是否符合条件,因为可能存在其他线程唤醒线程10。
进阶代码
public class MyThread {
//公共变量,用来标明通信
public static Integer num = 0;
public static final Object obj = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while(num != 9){
synchronized (obj){
try {
System.out.println(Thread.currentThread().getName()+":9号物品还没有搬走,我等等");
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(Thread.currentThread().getName()+":我搬走了10号物品");
System.out.println(Thread.currentThread().getName()+":我结束了");
}
},"10号工人").start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj){
System.out.println(Thread.currentThread().getName() + ":我搬走9号物品了。");
num = 9;
obj.notifyAll();
}
}
},"9号工人").start();
}
}
输出:
10号工人:9号物品还没有搬走,我等等
9号工人:我搬走9号物品了。
10号工人:我搬走了10号物品
10号工人:我结束了
notify和notifyAll的区别
根据方法说明,notify随机唤醒该对象监听器中等待线程的一个,而notifyAll是唤醒所有等待线程;正常我们要使用notifyAll,存在notify唤醒的线程因为某种原因,本身又wait了,那么该监视器的所有的线程都处于等待中,就都挂了。当然notifyAll还是有成本的,唤醒了不少无法执行的线程。
wait和sleep
wait:Object类的方法,当前线程处于等待状态,让出cpu和释放锁(后期再说)。
sleep:Thread类的方法,当前线程处于休息,并没有让出cpu,同时持有锁对象。
(2) 共享变量
就是多个线程通过同一个变量,进行通信,你在这儿放个信息,我到时候去取,这样就完成了一次通信,当然这是简单的通讯。
public class ThreadCommunication {
public static Integer num = 0;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while(num != 9){
System.out.println(Thread.currentThread().getName()+":9号物品还没有搬走,我等等");
}
System.out.println(Thread.currentThread().getName()+":我搬走了10号物品");
System.out.println(Thread.currentThread().getName()+":我结束了");
}
},"10号工人").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":我搬走9号物品了。");
num = 9;
}
},"9号工人").start();
}
}
输出:
10号工人:9号物品还没有搬走,我等等
10号工人:9号物品还没有搬走,我等等
10号工人:9号物品还没有搬走,我等等
10号工人:9号物品还没有搬走,我等等
10号工人:9号物品还没有搬走,我等等
9号工人:我搬走9号物品了。
10号工人:9号物品还没有搬走,我等等
10号工人:我搬走了10号物品
10号工人:我结束了
示例2
/**通过共享变量进行通讯,当已经完成的线程超过10个后,就启动特殊的线程。
或者指定的service执行完成,再执行特殊的线程。
***/
package com.thread.communication;
import java.util.Random;
/**通过共享变量进行通讯,注意这里的限制条件是:只要有5个线程完成了,特殊线程就可以执行了***/
public class ShareVariable {
public static void main(String[] args) {
final Signal signal = new Signal(5);
for(int i=0;i<20;i++){
new Thread(new Runnable() {
public void run() {
int i = new Random().nextInt(10000)+100;
while((i--)>0);
signal.complete();
}
}).start();
}
//特殊线程
new Thread(new Runnable() {
public void run() {
//循环判断已经执行的线程数是否满足
while(!signal.isComplete());
System.out.println(Thread.currentThread().getName()+" 终于可以干活了");
}
}).start();
System.out.println("---------");
}
}
class Signal{
private int totalNum;//需要完成线程的数量
private int completeNum;//已经完成的线程数量
public Signal(int totalNum){
this.totalNum = totalNum;
}
public synchronized void complete(){
System.out.println(Thread.currentThread().getName()
+" 完成工作 "+totalNum+","+completeNum);
if(totalNum > completeNum){
completeNum++;
}
}
public synchronized boolean isComplete(){
System.out.println(Thread.currentThread().getName()+"----");
return (totalNum == completeNum);
}
}
输出:
Thread-0 完成工作 5,0
Thread-1 完成工作 5,1
Thread-5 完成工作 5,2
Thread-9 完成工作 5,3
Thread-2 完成工作 5,4
Thread-6 完成工作 5,5
Thread-10----
Thread-10 终于可以干活了
Thread-4 完成工作 5,5
Thread-8 完成工作 5,5
Thread-3 完成工作 5,5
Thread-7 完成工作 5,5
从输出内容看:
会发现特殊线程是第7个执行的,因为我们的条件是,只要有5个线程完成,特殊线程就可以执行了,
注意这里是执行,并没有说特殊线程立刻执行,因为特殊线程还需要和其他线程进行锁的争夺,
这次的输出应该是没有争取到。
多执行几次,估计可以看到如下的输出:
Thread-1 完成工作 5,0
Thread-5 完成工作 5,1
Thread-2 完成工作 5,2
Thread-4 完成工作 5,3
Thread-10----
Thread-10----
Thread-8 完成工作 5,4
Thread-0 完成工作 5,5
Thread-10----
Thread-10 终于可以干活了
Thread-6 完成工作 5,5
Thread-9 完成工作 5,5
Thread-3 完成工作 5,5
会发现特殊线程执行了多次,但是只有当5个线程完成后,才会有机会执行特殊线程。
如果要求特殊线程必须第6个执行,那么就需要严格的处理代码逻辑,
1 totalNum和completeNum都需要volatile进行修改,同时普通线程执行的时候,
如果是第6个执行的线程,就需要等待,让特殊线程优先执行。
就是通过while循环,条件不符合就一直进行判断,直到条件符合,这样对cpu存在浪费,10号工人一直在空转(while判断)。
其他通信方式 等待下几篇。
总结
线程间的通信,如果通过wait和notify进行通信,需要注意以下情况:
- wait和notify必须在synchronized中。
- 获取同一对象的监视器,才能互相唤醒。
- 为了防止监视器对象发生变化,最好单独定义一个监视器对象,不参与任何业务逻辑,例如:private final Object obj = new Object();
- 线程等待的条件不要用if,使用while判断,保证唤醒后,判断条件是否符合,因为存在无效的唤醒(唤醒你并不是条件符合才唤醒你)。
- 当唤醒时,使用notifyAll方法,尽量不要使用notify方法。
- 通过共享变量进行通信时,限制比较多,请使用时代码逻辑要清晰。