为什么有线程间的通信,线程的通信是理解多线程执行的一个基本知识点,当线程执行到某一步骤时,因为条件不能满足继续执行的需求,需要等待资源满足后才能继续执行。这时线程需要释放它持有的资源让其他线程执行,当相关条件满足后其他线程会通知他继续执行。
线程间通信常常会伴随线程状态的改变,常见的状态改变有:RUNNABLE、BLOCKED、WAITING、TIMED_WAITING,下面总结了一下在java中能够引起以上某些状态改变的方法:
在线程的共享锁上执行wait方法,当前线程会由RUNNABLE状态进入WAITING(没有指定超时时间)或TIMED_WAITING(指定超时时间),这时线程会释放掉它所持有的锁。当线程进入WAITING状态后,必须有其他线程唤醒当前线程,它才能够继续执行,否则当前线程会一直处于 WAITING 状态,而处于 TIMED_WAITING 状态的线程当超时时间到了之后会自动唤醒。唤醒后的线程会重新竞争锁资源,获得锁后进入 RUNNABLE 状态,没有获得锁将进入 BLOCKED 状态。
必须注意一点,wait / notify、notifyAll 代码必须要放在同步代码块中执行,还有在执行 wait 方法后必须要保证能够执行 notify、notifyAll 方法,否则线程有可能会一直休眠。
在Object类中打开查看方法的源代码,这里用蹩脚的中文做了简单翻译:
/**
* 唤醒一个正在等待监视器的线程,如果有多个线程正在等待监控器,那么会随机选择一个被唤醒,
* 唤醒后的线程同其他线程一样需要竞争锁资源,在没有获得锁资源之前,线程会处于阻塞状态。
* 这个方法只能由作为监控对象的所有者线程调用,线程可以通过三种方式之一成为对象监视器所有者:
* 1、通过执行该对象的同步实例方法
* 2、通过执行对象上同步的 synchronized 语句体
* 3、对于 Class 类型对象,该类的同步静态方法
* 一次只能有一个线程拥有对象的监视器
*/
public final native void notify();
/**
* 唤醒等待此对象的所有线程,线程通过调用 wait 方法等待对象的监视器。
* 唤醒后的线程需要竞争锁资源,在没有获得锁之前,唤醒后的线程无法继续执行,
* 同 notify 方法一样,只能由此对象监视器所有者的线程调用
*/
public final native void notifyAll();
/**
* 使当前线程等待另一个线程调用 notify() 方法或 notifyAll() 方法唤醒,或者指定时间已过自动唤醒。
* 当前线程必须拥有此对象的监视器才能调用 wait 方法,所以该方法必须放在同步代码块中执行,
* 线程调用方法后将自身置于此对象的等待集中,然后放弃对象上的所有同步声明并处于休眠状态,直到以下四种情况之一发生:
* 1、其他线程调用此对象的 notify 方法,而当前线程恰好被选中为唤醒线程
* 2、其他线程调用此对象的 notifyAll 方法
* 3、其他线程调用 interrupt() 方法中断线程运行
* 4、指定超时时间已过
* 唤醒后的线程从该对象的等待集中移除,并重新启用线程调度,在竞争到锁后恢复到调用 wait 方法时的状态继续运行。
* 线程也可以在不被通知、中断或超时的情况下唤醒,即所谓的虚假唤醒。虽然这种情况很少发生但是在开发中必须防范,
* 所以等待应该总是在循环体中发生:
* synchronized (obj) {
* while (条件不成立)
* obj.wait(timeout);
* }
*
* wait 方法只会解锁当前对象,当前线程等待时,当前线程的其他对象仍然保持锁定。
* 同 notify 方法一样,只能由此对象监视器所有者的线程调用
*/
public final native void wait(long timeout) throws InterruptedException;
/**
* 与 wait(long timeout) 方法类似,只是多了一个纳秒为单位的超时时间,所以超时的时间还需要加上 nanos 纳秒。
*/
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
/**
* 没有指定超时时间,调用该方法的线程只能被其他线程通过 notify 或 notifyAll 方法唤醒,否则会一直等待
*/
public final void wait() throws InterruptedException {
wait(0);
}
通过上面的源代码可以看出,方法最终调用的是用 native 修饰的本地方法,在源代码中也指出了有关方法使用的更多信息,参考Doug Lea的《Concurrent Programming in Java》或Joshua Bloch的《Effective Java Programming Language Guide》里面的介绍。
下面示例一个 wait 的用法:启动多个线程,每个线程给定一个编号,当编号相同时,就执行打印任务,其他线程则等待,理想状态下输出顺序应该是 0 -> 1 -> 2 … 0 -> 1 -> 2:
import java.util.ArrayList;
import java.util.List;
/**
* 主线程
* @author xingo
* @date 2021/2/20
*/
public class TestPrintNumber {
public static final List threadList = new ArrayList();
public static void main(String[] args) throws InterruptedException {
int total = 3;
Number testNum = new Number(total);
for(int i = 0; i < total; ++i) {
PrintWorker printWorker = new PrintWorker(i, testNum);
Thread thread = new Thread(printWorker, "PrintWorker-" + i);
threadList.add(thread);
thread.start();
}
for(Thread thread : threadList) {
thread.join();
}
System.out.println("--------------------------------------------");
System.out.println("主线程 " + Thread.currentThread().getName() + " 输出所有的线程状态");
for(Thread thread : TestPrintNumber.threadList) {
System.out.println(thread.getName() + " : " + thread.getState());
}
}
}
/**
* 打印线程,当对象中的值与线程编号相等时,该线程打印数值并将下一个打印值+1
* @author xingo
* @date 2021/2/18
*/
class PrintWorker implements Runnable {
private int threadNum;
private Number testNum;
public PrintWorker(int threadNum, Number testNum) {
this.threadNum = threadNum;
this.testNum = testNum;
}
@Override
public void run() {
while (this.testNum.getNum() < 100) {
synchronized (this.testNum) {
System.out.println("---------------- " + Thread.currentThread().getName() + " 进入线程执行 ----------------");
// threadNum 与 printThread 相等,就将对象中的num值+1并打印出该值,同时通知下一个线程执行打印任务
if(this.threadNum == this.testNum.getPrintThread()) {
this.testNum.setNum(this.testNum.getNum() + 1);
System.out.println(Thread.currentThread().getName() + " ===>>> " + this.testNum.getNum());
int noticeThread = (this.threadNum + 1) % this.testNum.getTotalThread();
this.testNum.setPrintThread(noticeThread);
this.testNum.notify();
System.out.println("在线程 " + Thread.currentThread().getName() + " if中输出线程状态");
for(Thread thread : TestPrintNumber.threadList) {
System.out.println(thread.getName() + " : " + thread.getState());
}
} else { //如果threadNum 与 printThread 不等,就通知其他线程执行,同时该线程释放占用的资源
try {
this.testNum.notify(); //
this.testNum.wait();
System.out.println("在线程 " + Thread.currentThread().getName() + " else中输出线程状态");
for(Thread thread : TestPrintNumber.threadList) {
System.out.println(thread.getName() + " : " + thread.getState());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
/**
* 数值类
* @author xingo
* @date 2021/2/20
*/
class Number {
private int printThread = 0; //当前正在执行输出的线程序号
private int totalThread; //运行线程总数
private int num; //计数值
public Number(int totalThread) {
this.totalThread = totalThread;
}
public int getPrintThread() {
return printThread;
}
public void setPrintThread(int printThread) {
this.printThread = printThread;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public int getTotalThread() {
return totalThread;
}
}
上面的代码执行后的一次输出结果为:
(1)---------------- PrintWorker-0 进入线程执行 ----------------
PrintWorker-0 ===>>> 1
在线程 PrintWorker-0 if中输出线程状态
PrintWorker-0 : RUNNABLE
PrintWorker-1 : BLOCKED
PrintWorker-2 : BLOCKED
(2)---------------- PrintWorker-0 进入线程执行 ----------------
(3)---------------- PrintWorker-2 进入线程执行 ----------------
(4)---------------- PrintWorker-1 进入线程执行 ----------------
PrintWorker-1 ===>>> 2
在线程 PrintWorker-1 if中输出线程状态
PrintWorker-0 : BLOCKED
PrintWorker-1 : RUNNABLE
PrintWorker-2 : BLOCKED
(5)---------------- PrintWorker-1 进入线程执行 ----------------
(6)在线程 PrintWorker-2 else中输出线程状态
PrintWorker-0 : BLOCKED
PrintWorker-1 : WAITING
PrintWorker-2 : RUNNABLE
(7)---------------- PrintWorker-2 进入线程执行 ----------------
PrintWorker-2 ===>>> 3
在线程 PrintWorker-2 if中输出线程状态
PrintWorker-0 : BLOCKED
PrintWorker-1 : BLOCKED
PrintWorker-2 : RUNNABLE
(8)---------------- PrintWorker-2 进入线程执行 ----------------
(9)在线程 PrintWorker-0 else中输出线程状态
PrintWorker-0 : RUNNABLE
PrintWorker-1 : BLOCKED
PrintWorker-2 : WAITING
(10)---------------- PrintWorker-0 进入线程执行 ----------------
PrintWorker-0 ===>>> 4
在线程 PrintWorker-0 if中输出线程状态
PrintWorker-0 : RUNNABLE
PrintWorker-1 : BLOCKED
PrintWorker-2 : BLOCKED
........
---------------- PrintWorker-2 进入线程执行 ----------------
PrintWorker-2 ===>>> 99
在线程 PrintWorker-2 if中输出线程状态
PrintWorker-0 : BLOCKED
PrintWorker-1 : BLOCKED
PrintWorker-2 : RUNNABLE
---------------- PrintWorker-2 进入线程执行 ----------------
在线程 PrintWorker-1 else中输出线程状态
PrintWorker-0 : BLOCKED
PrintWorker-1 : RUNNABLE
PrintWorker-2 : WAITING
---------------- PrintWorker-1 进入线程执行 ----------------
在线程 PrintWorker-0 else中输出线程状态
PrintWorker-0 : RUNNABLE
PrintWorker-1 : WAITING
PrintWorker-2 : BLOCKED
---------------- PrintWorker-0 进入线程执行 ----------------
PrintWorker-0 ===>>> 100
在线程 PrintWorker-0 if中输出线程状态
PrintWorker-0 : RUNNABLE
PrintWorker-1 : BLOCKED
PrintWorker-2 : BLOCKED
在线程 PrintWorker-2 else中输出线程状态
PrintWorker-0 : RUNNABLE
PrintWorker-1 : BLOCKED
PrintWorker-2 : RUNNABLE
在线程 PrintWorker-1 else中输出线程状态
PrintWorker-0 : TERMINATED
PrintWorker-1 : RUNNABLE
PrintWorker-2 : TERMINATED
--------------------------------------------
主线程 main 输出所有的线程状态
PrintWorker-0 : TERMINATED
PrintWorker-1 : TERMINATED
PrintWorker-2 : TERMINATED
Process finished with exit code 0
现在分析一下执行结果:
(1)首先序号为0的线程先执行,执行完后通知其他线程执行。
(2)由于所有线程都是RUNNABLE状态,执行完的线程0又抢到资源进入方法,但此时线程0不满足条件执行了else方法:首先通知其他处于 WAITING 状态的线程,然后执行 wait() 方法,由于此时没有线程处于 WAITING 状态,这时所有线程去争夺锁资源,获得锁资源的线程将进入同步方法执行。
(3)线程2得到了锁资源进入同步方法执行,但由于线程2不满足if条件就走了else分支也进入了 WAITING 状态。
(4)线程1抢到资源进入同步方法执行了if方法,执行完成后通知其他线程执行,从打印结果可以看出,除了当前线程其他两个线程都处于 BLOCKED 状态,本来处于 WAITING 状态的线程0 被线程2的 notify() 方法唤醒,处于 WAITING 状态的线程2被线程1的 notify() 方法唤醒。
(5)线程1抢到锁资源进入方法执行,但是不满足if条件执行了else方法进入 WAITING 状态。
(6)处于 WAITING 状态的线程2继续执行else方法的剩余代码,输出所有的线程状态。
(7)执行完后的线程2又获得锁资源继续进入同步方法执行,这时的线程2满足if条件进入分支运行。
(8)线程2又抢到资源进入同步方法,但是此时线程2也不满足if条件执行了else分支进入 WAITING 状态。
(9)线程0被唤醒继续执行 else 方法。
(10)线程0抢到资源进入同步方法执行if分支 …
end: 所有线程在以上的步骤中循环往复的运行,直到线程到达TERMINATED状态。
分析上面的代码可知,线程的运行状态是无序的,也是不可预测的。
线程的sleep方法定义在Thread类中,打开Thread类可以看到sleep方法的定义如下:
/**
* 使当前执行的线程休眠指定的毫秒数,线程在休眠时不会失去监视器的所有权
*/
public static native void sleep(long millis) throws InterruptedException;
/**
* 使当前执行的线程休眠指定的毫秒数加上纳秒数,线程在休眠时不会失去监视器的所有权
*/
public static void sleep(long millis, int nanos) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
可见sleep方法也是用 native 关键字修饰的,表示这是本地方法,也就是需要操作系统帮忙实现,java语言只管调用就可以了。
sleep方法经常用于和wait方法比较使用,他们都能够让当前线程停止运行,但是他们又有一些不同,大致总结有以下几个方面:
1、wait 是对象方法,而 sleep 是类方法。
2、wait 使用时必须在同步代码块中,而 sleep 不需要。
3、wait 方法进入 WAITING 状态后会释放对象的锁,而 sleep 不会释放锁资源。
4、wait 进入 WAITING 状态后的能够被 notify 或 notifyAll 唤醒,而 sleep 不能被其他线程唤醒,只能等到超时时间后自动执行。
5、wait 常常用于线程间的通信,而 sleep 更多的用于让当前线程停下来一会儿,停完后继续运行。
对比两者的不同,其他几点都比较容易理解,我们主要验证一下第3点,比较一下两个方法暂停运行后对锁资源的处理方式:
两个方法都在同步代码块中分别休眠一段时间输出内容:
sleep方法
import java.util.ArrayList;
import java.util.List;
/**
* @author xingo
* @date 2021/2/25
*/
public class TestSleep {
public static final List list = new ArrayList();
public static void main(String[] args) {
for(int i = 0; i < 2; ++i) {
Thread thread = new Thread(new SleepThread(), "thread" + i);
list.add(thread);
thread.start();
}
}
}
class SleepThread implements Runnable {
@Override
public void run() {
synchronized (TestSleep.class) {
System.out.println("===>>> " + Thread.currentThread().getName() + " 休眠前输出线程状态 :");
for(Thread thread : TestSleep.list) {
System.out.println(thread.getName() + " 线程当前状态 : " + thread.getState());
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("===>>> " + Thread.currentThread().getName() + " 休眠后输出线程状态 :");
for(Thread thread : TestSleep.list) {
System.out.println(thread.getName() + " 线程当前状态 : " + thread.getState());
}
}
}
}
输出内容:
===>>> thread0 休眠前输出线程状态 :
thread0 线程当前状态 : RUNNABLE
thread1 线程当前状态 : RUNNABLE
===>>> thread0 休眠后输出线程状态 :
thread0 线程当前状态 : RUNNABLE
thread1 线程当前状态 : BLOCKED
===>>> thread1 休眠前输出线程状态 :
thread0 线程当前状态 : RUNNABLE
thread1 线程当前状态 : RUNNABLE
===>>> thread1 休眠后输出线程状态 :
thread0 线程当前状态 : TERMINATED
thread1 线程当前状态 : RUNNABLE
Process finished with exit code 0
wait方法
import java.util.ArrayList;
import java.util.List;
/**
* @author xingo
* @date 2021/2/25
*/
public class TestWait {
public static final List list = new ArrayList();
public static void main(String[] args) {
for(int i = 0; i < 2; ++i) {
Thread thread = new Thread(new WaitThread(), "thread" + i);
list.add(thread);
thread.start();
}
}
}
class WaitThread implements Runnable {
@Override
public void run() {
synchronized (TestWait.class) {
System.out.println("===>>> " + Thread.currentThread().getName() + " 休眠前输出线程状态 :");
for(Thread thread : TestWait.list) {
System.out.println(thread.getName() + " 线程当前状态 : " + thread.getState());
}
try {
TestWait.class.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("===>>> " + Thread.currentThread().getName() + " 休眠后输出线程状态 :");
for(Thread thread : TestWait.list) {
System.out.println(thread.getName() + " 线程当前状态 : " + thread.getState());
}
}
}
}
输出内容:
===>>> thread0 休眠前输出线程状态 :
thread0 线程当前状态 : RUNNABLE
thread1 线程当前状态 : RUNNABLE
===>>> thread1 休眠前输出线程状态 :
thread0 线程当前状态 : TIMED_WAITING
thread1 线程当前状态 : RUNNABLE
===>>> thread0 休眠后输出线程状态 :
thread0 线程当前状态 : RUNNABLE
thread1 线程当前状态 : TIMED_WAITING
===>>> thread1 休眠后输出线程状态 :
thread0 线程当前状态 : RUNNABLE
thread1 线程当前状态 : RUNNABLE
Process finished with exit code 0
从结果输出可知,在调用sleep方法后其他线程并不能进入同步方法执行,也就是锁资源没有释放;而wait方法执行后其他线程能够进入同步方法。这也验证了两个方法对锁资源的处理方式。
这两个方法定义在LockSupport类中,用于对线程进行阻塞和解除阻塞的作用,与wait/notify方法不用的是,他们不需要放入同步代码块中执行,也不需要保证调用顺序的先后。当unpark在park之前调用,再次调用park方法时当前线程也不会被阻塞。这是因为:
1、unpark调用时,如果当前线程还未进入park,则许可为true。
2、park调用时,判断许可是否为true:如果是true,则继续往下执行;如果是false,则等待,直到许可为true。
在LockSupport类中有多个park/unpark的重载方法,功能相似,现在只选择两个做一下注释:
/**
*
* 如果许可可用,该方法会立即返回;否则当前线程将进入休眠状态,直到发生以下情况之一会继续运行:
* 其他线程以当前线程为目标调用了unpark()方法;
* 其他线程中断了当前线程;
* 线程发生错误
*
* 此方法不报告导致方法返回的原因,所以在park()方法继续运行后,调用者应该首先重新检查当前线程的中断状态
*/
public static void park() {
UNSAFE.park(false, 0L);
}
/**
* 使给定线程的许可证可用:
* 如果线程在park()上被阻塞,那么它将解除阻塞;否则下一次调用park()保证不会阻塞。
* 如果给定的线程尚未启动,则不能保证此操作有任何效果(也就是再次调用park()方法后仍可能会阻塞)。
*/
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
使用时有一点需要注意,就是park方法后要判断线程状态,下面示例一个使用
import java.util.concurrent.locks.LockSupport;
/**
* @author xingo
* @date 2021/3/23
*/
public class ParkAndUnpark {
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(new ParkThread(), "thread-01");
t1.start();
Thread.sleep(20);
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + " 线程中执行了unpark方法");
}
}
class ParkThread implements Runnable {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 线程中输出内容");
Thread.sleep(500);
LockSupport.park();
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " 线程被中断了");
}
System.out.println(Thread.currentThread().getName() + " 线程中执行了park方法后继续运行");
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出内容:
thread-01 线程中输出内容
main 线程中执行了unpark方法
thread-01 线程中执行了park方法后继续运行
Process finished with exit code 0
这段代码模拟了先执行unpark方法后再执行park方法,可见线程并没有被阻塞,如果线程先执行了park方法,那么就需要其他线程对当前线程执行unpark方法才能让当前线程继续运行。这区别于wait/notify,调用了wait方法的线程必须被其他线程notify后才能再次运行,并且一定要保证wait后有其他线程执行notify方法,如果notify方法在wait方法之前执行,那么该线程也不能被唤醒。
这三个方法都是定义在Thread类中,由于已经被标记为Deprecated,也就不必过多研究。他们也是能够改变线程的状态,但是由于会引发死锁等相关问题,所以不建议使用:
join方法定义在Thread线程中,它是让当前线程等待其他线程执行完成后才能继续执行,在其他线程执行过程中,当前线程将被阻塞。
在Thread方法中join方法的定义:
/**
* 等待某个线程执行完成,并指定超时时间
*/
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) { //由于是通过子线程对象调用的join()方法,所以这里判断的是子线程是否存活,如果存活主线程就等待
wait(0); //这里等待的是主线程,当子线程执行完成后会调用notifyAll方法唤醒所有线程。所以不建议在子线程上面使用wait、notify或notifyAll相关的方法
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
/**
* 在等待时间上在加一个纳秒时间
*/
public final synchronized void join(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
}
/**
* 等待线程执行完成
*/
public final void join() throws InterruptedException {
join(0);
}
这个方法很简单,举个例子就明白了
/**
* @author xingo
* @date 2021/3/23
*/
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new JoinThread(), "thread-01");
t1.start();
t1.join();
System.out.println(Thread.currentThread().getName() + " 线程中输出内容");
}
}
class JoinThread implements Runnable {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 线程中输出内容");
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出内容:
thread-01 线程中输出内容
main 线程中输出内容
Process finished with exit code 0
正常情况下,如果main方法中没有调用子线程的join方法,那么主线程的输出会早于子线程的输出,join方法后主线程会等待子线程执行完成后再继续运行。这里需要注意一点,join方法要在子线程执行了start()方法后再调用,否则阻塞将不起作用。
yeild是定义在Thread类中的静态方法,他并不能让当前进行进入阻塞状态,只是让当前线程重新进入就绪状态去抢占CPU的调度权,这个时候它并不会释放获取到的锁资源,也不接受中断。在平时开发中使用的场景比较少,如果一定要用它的话,就是在那种复杂的任务执行过程中,担心当前线程长时间占用CPU,可以调用yeild让出CPU的调度权,等下次获取到再继续执行,这样不但能完成自己的任务,也能给其他线程一些运行的机会。
在Thread中方法的定义如下:
/**
* 表示当前线程放弃对处理器的使用,也就是释放CPU的时间片,使当前线程重新变成就绪状态,并重新竞争CPU的调度权,
* 这样当前线程有可能获取到CPU也有可能获取不到CPU
*/
public static native void yield();
由于方法简单没啥使用场景,也就不举例了。