大家好啊,我是小张,今天介绍一下Thread和Object类中和线程相关的重要方法这一章节的第一部分-----wait()、notify()、notifyAll()方法
``
1.wait()/notify()/notifyAll()方法详解
1.1. 方法概览
类 | 方法名 | 简介 |
---|---|---|
Thread | sleep相关 | "相关"指的是重载,例如sleep有多个方法,参数不同,实际作用大同小异 |
Thread | join | 等待其他线程执行完毕 |
Thread | yield相关 | 放弃已经获取到的CPU资源 |
Thread | currentThread | 获取当前线程的引用 |
Thread | start/run相关 | 启动线程相关 |
Thread | interrupt相关 | 中断线程 |
Thread | stop()/suspend()/resuem() 相关 | 已废弃 |
Object | wait()/notify()/notifyAll() 相关 | 让线程暂时休息和唤醒 |
1.2.wait()/notify()/notifyAll()作用
1.2.1 阻塞阶段:
如果某个线程调用wait方法,那么这个线程会释放掉获取的锁并进入Waiting状态,注意wait方法只能在synchronized代码块内调用,直到以下4种情况才会被唤醒:
- 另外一个线程调用这个对象的notify()方法且刚好被唤醒的是本线程;
- 另一个线程调用这个对象的notifyAll()方法;
- 过了wait(long time)规定的超时时间,如果传入0就是永久等待;
- 线程自身调用了interrupt()。
1.2.1.1. 另一个线程调用这个对象的notify()方法且刚好被唤醒的是本线程
package threadcoreknowlodge.threadobjectclasscommonmethods;
/**
* @author zhl
* @date 2022/12/23 19:43
* @description: 展示wait和notify的基本用法 1. 研究代码执行顺序 2. 证明wait释放锁
*/
public class Wait {
public static Object object = new Object();
static class Thread1 extends Thread {
@Override
public void run() {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "开始执行了");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁。");
}
}
}
static class Thread2 extends Thread {
@Override
public void run() {
synchronized (object) {
object.notify();
System.out.println("线程" + Thread.currentThread().getName() + "调用了notify()");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.start();
Thread.sleep(200);
thread2.start();
}
}
执行结果:
Thread-0开始执行了
线程Thread-1调用了notify()
线程Thread-0获取到了锁。
Process finished with exit code 0
需要注意的是另一个线程执行完这个对象的notify()方法之后,该线程并不会立马得到这把锁,而是需要重新去抢锁
1.2.1.2. 另一个线程调用这个对象的notifyAll()方法
package threadcoreknowlodge.threadobjectclasscommonmethods;
/**
* @author zhl
* @date 2022/12/24 10:44
* @description:3个线程,线程1和线程2首先被阻塞,线程3唤醒它们。notify, notifyAll。 start先执行不代表线程先启动。
*/
public class WaitNotifyAll implements Runnable{
private static final Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
WaitNotifyAll waitNotifyAll = new WaitNotifyAll();
Thread thread1 = new Thread(waitNotifyAll);
Thread thread2 = new Thread(waitNotifyAll);
thread1.start();
thread2.start();
Thread.sleep(200); // 睡眠200ms是等线程1 线程2都抢到锁执行完毕
new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
resourceA.notifyAll();
//resourceA.notify();
System.out.println("ThreadC notified.");
}
}
}).start();
}
@Override
public void run() {
synchronized (resourceA){
System.out.println(Thread.currentThread().getName()+"got resourceA lock.");
try {
System.out.println(Thread.currentThread().getName()+"waits to start.");
resourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"wait to end.");
}
}
}
执行结果:
Thread-0got resourceA lock.
Thread-0waits to start.
Thread-1got resourceA lock.
Thread-1waits to start.
ThreadC notified.
Thread-1wait to end.
Thread-0wait to end.
可以看到执行notifyAll()方法,所有线程都会被唤醒,如果执行notify()方法的话,只有单个线程被唤醒,但不确定被唤醒的是线程1还是线程2,这个由JVM决定。另外,之所以在线程1和线程2启动之后,睡眠200ms,是因为要线程1线程2都抢到这把锁,并执行完其中的代码
- 过了wait(long timeout)规定的超时时间,传入0就是永久等待
package threadcoreknowlodge.threadobjectclasscommonmethods;
/**
* @author zhl
* @date 2022/12/23 19:43
* @description: 展示wait(long time) 超时时间已过,重新获取到锁
*/
public class WaitLongTime {
public static Object object = new Object();
static class Thread1 extends Thread {
@Override
public void run() {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "拿到了lock.");
System.out.println(Thread.currentThread().getName() + "开始等待5s的超时时间");
try {
object.wait(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "重新获取到了锁。");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread1 thread1 = new Thread1();
thread1.start();
}
}
执行结果:
Thread-0拿到了lock.
Thread-0开始等待5s的超时时间
线程Thread-0重新获取到了锁。
Process finished with exit code 0
可以看到线程Thread-0是在等待超时时间过去之后,又重新获取到了锁
- 线程自身调用了interrupt()
1.2.2 唤醒阶段:
notify()方法是随机唤醒一个线程,notifyAll()方法唤醒所有等待的线程,注意wait()和notify()方法都是在synchronized保护的代码块中执行。
1.2.3 中断阶段:
如果线程被中断,那么会抛出interruptedException,并且释放掉获取的锁。
1.3.wait,notify,notifyAll特点、性质
- 使用必须先拥有monitor
- notify()只能唤醒其中一个
- 属于Object类
- 同时持有多个锁的情况
1.4.wait原理
- 线程2获取到了锁
- 线程2执行完毕后释放掉了锁,也就是线程6对应的状态,running->terminated
- 如果线程2释放掉了锁,也就是线程3对应的状态,相当于线程2执行了wait方法
- 然后线程会处于Wait Set状态,直到被其他线程notify,然后线程重新竞争锁,竞争的时候处于blocked状态,直到获取锁,并处于Running状态,最后执行完代码,终止线程
1.5.wait()/notify()/notifyAll()常见面试问题
- 用wait/notify实现生产者消费着设计模式
- 两个线程交替打印0~200奇偶数
- 为什么wait()需要在同步代码块内使用,而sleep不需要?
- 为什么线程通信的方法wait(),niotify()和notifyAll()被定义在Object类里?而sleep定义在Thread类里?
- wait是属于Object对象的,那么调用Thread.wait会怎么样?
- 如何选择notify()还是notifyAll()?
- notifyAll()之后所有线程都会再次抢锁,如果某线程抢夺失败怎么办?
1.5.1. 用wait/notify实现生产者消费者设计模式
这个很重要,请注意
package threadcoreknowlodge.threadobjectclasscommonmethods;
import java.util.Date;
import java.util.LinkedList;
/**
* @author zhl
* @date 2022/12/24 14:37
* @description: 用wait/notify来实现生产者消费者模式
*/
public class ProducerConsumerModel {
public static void main(String[] args) {
EventStorage eventStorage = new EventStorage();
Producer producer = new Producer(eventStorage);
Consumer consumer = new Consumer(eventStorage);
new Thread(producer).start();
new Thread(consumer).start();
}
}
class Producer implements Runnable{
private EventStorage storage;
public Producer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.put();
}
}
}
class Consumer implements Runnable{
private EventStorage storage;
public Consumer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.take();
}
}
}
class EventStorage{
private int maxSize;
private LinkedList<Date> storage;
public EventStorage() {
this.maxSize = 10;
this.storage = new LinkedList<>();
}
/**
* 生产者生产
*/
public synchronized void put(){
// 当storage满了的时候,生产者停止生产,让线程处于waiting
while (storage.size()==maxSize){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没满,生产者继续生产
storage.add(new Date());
System.out.println("仓库里有了" + storage.size() + "个产品。");
notify();
}
/**
* 消费者消费
*/
public synchronized void take(){
// 当storage==0的时候,停止消费
while (storage.size()==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果不等于0,就进行消费
Date date = storage.get(0);
storage.remove(0);
System.out.println("拿到了 "+date+",现在仓库还剩下"+storage.size());
notify();
}
}
1.5.2. 两个线程交替打印0~200奇偶数
这个有2种实现方式,我们分别进行演示,并进行优缺点的比较
1.5.2.1 用synchronized实现
package threadcoreknowlodge.threadobjectclasscommonmethods;
/**
* @author zhl
* @date 2022/12/24 15:14
* @description: 两个线程交替打印0~100的奇偶数,用synchronized关键字实现
* 缺点:效率不高因为两个线程要不停的抢锁,且同时只有一个线程在执行
*/
public class WaitNotifyPrintOddEvenSyn {
private static int count =0;
private static final Object lock = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (count<100){
synchronized (lock){
if (count%2==0){
System.out.println(Thread.currentThread().getName()+":"+count++);
}
}
}
}
},"偶数").start();
new Thread(new Runnable() {
@Override
public void run() {
while (count<100){
synchronized (lock){
if (count%2==1){
System.out.println(Thread.currentThread().getName()+":"+count++);
}
}
}
}
},"奇数").start();
}
}
说明:这种方式虽然也能实现交替打印,但是效率不高,因为两个线程在不停的争抢锁,不建议使用这种方式。但是这种方式也有一定的优化空间,比如我们在判断是奇偶数的时候,可以使用位运算,我们可以将count%2==0换成(count & 1 ) ==0
1.5…2.2 用wait/notify实现
package threadcoreknowlodge.threadobjectclasscommonmethods;
/**
* @author zhl
* @date 2022/12/24 15:09
* @description: 两个线程交替打印奇偶数 wait/notify
*/
public class WaitNotifyPrintOddEveWait {
public static void main(String[] args) {
TurningRunner turningRunner = new TurningRunner();
new Thread(turningRunner, "偶数").start();
new Thread(turningRunner, "奇数").start();
}
}
//1. 拿到锁,我们就打印
//2. 打印完,唤醒其他线程,自己就休眠
class TurningRunner implements Runnable {
private static int count = 0;
private static final Object lock = new Object();
@Override
public void run() {
while (count <= 100) {
synchronized (lock) {
//拿到锁就打印
System.out.println(Thread.currentThread().getName() + ":" + count++);
lock.notify();
if (count <= 100) {
try {
//如果任务还没结束,就让出当前的锁,并休眠
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
说明这种方式不需要抢锁,所以会快一些
1.5.3. 为什么wait()需要在同步代码块内使用,而sleep不需要?
主要是为了让通信变得可靠,防止死锁或永久等待的发生,如果没有synchronized 的保护,那么wait和notify就可以随意的切换,sleep只和自己相关,所以不需要放在synchronized 代码块内
1.5.4. 为什么线程通信的方法wait(),niotify()和notifyAll()被定义在Object类里?而sleep定义在Thread类里?
首先wait(),niotify()和notifyAll()是锁级别的操作,而锁是绑定对象的,我们要求锁是对于每个对象都是适用的,所以将被定义在了Object类里
1.5.5. wait是属于Object对象的,那么调用Thread.wait会怎么样?
相当于没有调用,因为调用wait之后,会马上调用notify
1.5.6. 如何选择notify()还是notifyAll()?
如果是唤醒单个线程可以使用notify(),如果是多个线程可以使用notifyAll()
1.5.7. notifyAll()之后所有线程都会再次抢锁,如果某线程抢夺失败怎么办?
如果某线程抢夺失败,那么就会回到Blocked状态,等待某线程释放锁,重新抢夺
2.线程的6种状态
- 创建好新城处于New状态
- 执行start方法之后就处于Running状态
- 线程执行完代码块之后处于Terminated状态
- 当线程在获取到monitor 锁之前,也就是进入synchronized关键字修饰的代码块或方法之前处于Blocked状态,一旦线程拿到了monitor锁之后就处于了Running状态
- 一旦线程执行了Object.wait()/Thread.join()等方法,线程就会进入wait状态,直到其他线程执行了相同锁的notify()方法
需要补充的是:
- 线程从Object.wait()状态刚被唤醒时,通常不能立刻抢到monitor锁,那就会从waiting状态先进入Blocked状态,抢到锁之后再转到Running状态。
- 如果发生异常,可以直接跳到Terminated状态,比如可以从waiting()状态直接到Terminated状态
3.sleep方法详解
3.1. 不释放锁
sleep方法的特点是不释放锁,无论是lock还是synchronized ,只有等sleep时间到了之后,才释放锁
3.1.1 展示线程sleep的时候不释放synchronized的monitor
package threadcoreknowlodge.threadobjectclasscommonmethods;
/**
* @author zhl
* @date 2022/12/24 17:04
* @description: 展示线程sleep的时候不释放synchronized的monitor,等sleep时间到了以后,正常结束后才释放锁
*/
public class SleepDontReleaseMonitor implements Runnable{
public static void main(String[] args) {
SleepDontReleaseMonitor monitor = new SleepDontReleaseMonitor();
new Thread(monitor).start();
new Thread(monitor).start();
}
@Override
public void run() {
sync();
}
public synchronized void sync(){
System.out.println(Thread.currentThread().getName()+" 获取到了monitor。");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 退出了同步代码块。");
}
}
执行结果
Thread-0 获取到了monitor。
Thread-0 退出了同步代码块。
Thread-1 获取到了monitor。
Thread-1 退出了同步代码块。
可以看出Thread-0执行完了之后才轮到Thread-1,所以线程在sleep的时候并没有释放锁
3.1.2 演示sleep不释放lock(lock需要手动释放)
package threadcoreknowlodge.threadobjectclasscommonmethods;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author zhl
* @date 2022/12/24 17:13
* @description: 演示sleep不释放lock(lock需要手动释放)
*/
public class SleepDontReleaseLock implements Runnable{
private static final Lock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();
System.out.println("线程"+Thread.currentThread().getName()+" 获取到了锁");
try {
Thread.sleep(5000);
System.out.println("线程" + Thread.currentThread().getName() + " 睡眠后已经苏醒");
} catch (InterruptedException e) {
e.printStackTrace();
}finally { // 最后一定要释放锁
lock.unlock();
}
}
public static void main(String[] args) {
SleepDontReleaseLock sleepDontReleaseLock = new SleepDontReleaseLock();
new Thread(sleepDontReleaseLock).start();
new Thread(sleepDontReleaseLock).start();
}
}
执行结果
线程Thread-0 获取到了锁
线程Thread-0 睡眠后已经苏醒
线程Thread-1 获取到了锁
线程Thread-1 睡眠后已经苏醒
Process finished with exit code 0
同样可以看到线程在sleep之后,也没有释放lock
3.2. 响应中断
- 抛出InterruptedException
- 清除状态
package threadcoreknowlodge.threadobjectclasscommonmethods;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @author zhl
* @date 2022/12/24 17:42
* @description: 每个1秒钟输出当前时间,被中断,观察。
* Thread.sleep()
* TimeUnit.SECONDS.sleep()
*/
public class SleepInterrupted implements Runnable{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new SleepInterrupted());
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
@Override
public synchronized void run() {
if (!Thread.interrupted()){
for (int i = 0; i < 10; i++) {
System.out.println(new Date());
try {
Thread.sleep(1000);
// TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println("我被中断了!");
e.printStackTrace();
}
}
}
}
}
Sat Dec 24 18:08:12 CST 2022
Sat Dec 24 18:08:13 CST 2022
Sat Dec 24 18:08:14 CST 2022
Sat Dec 24 18:08:15 CST 2022
Sat Dec 24 18:08:16 CST 2022
我被中断了!
Sat Dec 24 18:08:17 CST 2022
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at threadcoreknowlodge.threadobjectclasscommonmethods.SleepInterrupted.run(SleepInterrupted.java:27)
at java.lang.Thread.run(Thread.java:748)
Sat Dec 24 18:08:18 CST 2022
Sat Dec 24 18:08:19 CST 2022
Sat Dec 24 18:08:20 CST 2022
Sat Dec 24 18:08:21 CST 2022
Process finished with exit code 0
可以看到在子线程睡眠的时候响应了中断,抛出了InterruptedException异常,并且当前线程清除了状态
一句话总结sleep就是:sleep可以让线程进入Waiting状态,并且不占用CPU资源,但是不释放锁,直到规定时间后执行,休眠期间如果被中断,会抛出异常,并清除中断状态
3.3. sleep常见面试问题
3.3.1. wait()/notify()、sleep异同
相同:
- 阻塞
- 响应中断
不同:
- 同步方法中
- 释放锁
- 指定时间
- 所属类
4. join方法详解
4.1. join方法作用、用法
因为新的线程加入我们,所以我们要等待他执行完再出发
注意:是主线程等待子线程
4.2. 代码演示
4.2.1. 普通用法
主要演示的是主线程等待子线程运行完毕,才开始执行自己的代码
package threadcoreknowlodge.threadobjectclasscommonmethods;
/**
* @author zhl
* @date 2022/12/24 18:40
* @description: 演示join,注意语句输出顺序,会变化。
*/
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});
thread1.start();
thread2.start();
System.out.println("开始等待子线程运行完毕");
thread1.join();
thread2.join();
System.out.println("所有子线程执行完毕");
}
}
执行结果:
开始等待子线程运行完毕
Thread-1执行完毕
Thread-0执行完毕
所有子线程执行完毕
Process finished with exit code 0
如果我们将 thread1.join(); thread2.join(); 进行注释,来一起看看结果
执行结果:
开始等待子线程运行完毕
所有子线程执行完毕
Thread-0执行完毕
Thread-1执行完毕
Process finished with exit code 0
可以看到先执行了主线程的代码,然后才执行的子线程的代码
4.2.2. 遇到中断
首先我们在子线程中中断主线程,但是子线程还是会继续运行,这种做法是不可取的,我们来演示一下
package threadcoreknowlodge.threadobjectclasscommonmethods;
/**
* @author zhl
* @date 2022/12/24 18:50
* @description: 演示join期间被中断的效果
*/
public class JoinInterrupt {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
// TODO 在子线程中 中断主线程
mainThread.interrupt();
Thread.sleep(5000);
System.out.println("线程"+Thread.currentThread().getName()+" finished.");
} catch (InterruptedException e) {
System.out.println("子线程中断");
}
}
});
thread.start();
System.out.println("等待子线程运行完毕.");
try {
thread.join();
} catch (InterruptedException e) {
System.out.println("线程"+Thread.currentThread().getName()+" 中断了。");
e.printStackTrace();
}
System.out.println("子线程已经运行完毕.");
}
}
执行结果:
等待子线程运行完毕.
线程main 中断了。
子线程已经运行完毕.
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Thread.join(Thread.java:1252)
at java.lang.Thread.join(Thread.java:1326)
at threadcoreknowlodge.threadobjectclasscommonmethods.JoinInterrupt.main(JoinInterrupt.java:29)
线程Thread-0 finished.
Process finished with exit code 0
可以看到主线程中断之后,子线程还在继续运行,那么我们该如何解决呢?
我们应该让子线程也中断,只需要在抓取到主线程中断之后,将中断进行传递
也就是子线程也进行中断 thread.interrupt();
4.2.3. join期间,线程是什么状态?waiting
package threadcoreknowlodge.threadobjectclasscommonmethods;
/**
* @author zhl
* @date 2022/12/24 19:39
* @description: 先join再mainThread.getState()
* 通过debugger看线程join前后状态的对比
*/
public class JoinThreadState {
public static void main(String[] args) throws InterruptedException {
Thread mainThread = Thread.currentThread();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
// TODO 用子线程获取主线程状态
System.out.println("线程"+mainThread.getName()+"状态:"+mainThread.getState());
System.out.println("Thread-0运行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
System.out.println("等待子线程运行完毕");
thread.join();
System.out.println("子线程运行完毕");
}
}
执行结果:
等待子线程运行完毕
线程main状态:WAITING
Thread-0运行结束
子线程运行完毕
Process finished with exit code 0
4.3. 原理
4.3.1. 源码
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()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
4.3.2. 分析
可以看到join底层调用了wait()方法,当millis == 0的时候,调用了wait(0),我们知道wait(0)是永久等待的意思,但是这里并没有看到任何notify(),那它是怎么唤醒线程的呢?这个是有JVM底层实现的。
只需要知道底层每个Thread执行完run()代码之后,会调用notifyAll()的方法
4.3.3. 等价
package threadcoreknowlodge.threadobjectclasscommonmethods;
/**
* @author zhl
* @date 2022/12/24 19:56
* @description: 通过讲解join原理,分析出join的代替写法
*/
public class JoinPrinciple {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});
thread.start();
System.out.println("开始等待子线程运行完毕");
thread.join();
// synchronized (thread) {
// thread.wait();
// }
System.out.println("所有子线程执行完毕");
}
}
执行结果:
开始等待子线程运行完毕
Thread-0执行完毕
所有子线程执行完毕
Process finished with exit code 0
join等同于 synchronized (thread) { thread.wait(); }