一、Threa和Object类简介
Threa和Object类方法概览 | ||
---|---|---|
类 | 方法名 | 简介 |
Thread | sleep相关 | 所谓相关,是指重载方法,方法名相同,但是参数不同,在本表格表达为相关,例如sleep有多个方法,只是参数不一样,实则作用大同小异 |
join | 等待其他线程执行完毕 | |
yield | 放弃已经获取到的cpu资源 | |
currentThread | 获取当前执行线程的引用 | |
start、run相关 | 启动线程相关 | |
interrupt相关 | 中断线程 | |
stop()、suspend()、resume()相关 | 已经废弃的方法 | |
Object | wait/notify/notifyAll相关 | 让线程暂时休息和唤醒 |
二、wait、notify、notifyAll方法详解
1、作用
- 阻塞阶段
- 唤醒阶段
- 遇到中断
有的时候我们希望一个线程或则多个线程去“休息”下,等到我们后续需要它的时候,或则是条件成熟的时候,再去唤醒它,这个就是我们wait、notify、notifyAll的作用。也就是可以控制一些线程去等待或则被唤醒,一旦一个线程进入等待就是进入了阻塞阶段,执行这个wait方法的时候,必须先拥有这个对象的monitor锁。
2、被唤醒的四种情况
- 另外一个线程调用这个对象的notify()方法且刚好被唤醒的是本线程;
- 另外一个线程调用这个对象的notifyAll()方法
- 过了wait(long time)规定的超时时间,如果传入0就是永久等待
- 线程自身调用了interrupt()
notify会唤醒单个正在等待某对象monitor的线程,唤醒的时候如果有多个线程都在等待,它只会选取其中一个,而具体唤醒的选择是任意的,java并没对此有严格的规范,JVM可以有自己的裁量权,notify和wait都需要在我们synchronized关键字保护的代码块或则方法中去执行,synchronized外面执行的话是会抛出异常的。一旦线程被唤醒就会重新参与线程的调度,可以继续执行了。
notifyAll也主要是起一个唤醒的作用,区别在于会把所有等待这个对象monitor的线程都一次性的唤醒。
如果一个线程已经执行了wait方法,因为wait方法是可以响应中断信号的,在此期间如果被中断的话,会抛出中断异常,并且释放目前已经获取到的这个monitor。
3、wait、notify的基本使用代码示例
证明wait是要释放monitor锁的,并且wait之后只要被notify线程就可以继续执行
public class Wait {
private static final Object LOCK = new Object();
static class Thread1 extends Thread {
@Override
public void run() {
synchronized (LOCK) {
try {
System.out.println(Thread.currentThread().getName() + "准备释放锁");
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完了。");
}
}
}
static class Thread2 extends Thread {
@Override
public void run() {
synchronized (LOCK) {
LOCK.notify();
System.out.println(Thread.currentThread().getName() + "执行完了!");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread1 thread1 = new Thread1();
thread1.setName("线程1");
Thread2 thread2 = new Thread2();
thread2.setName("线程2");
thread1.start();
Thread.sleep(1000);
thread2.start();
}
}
线程1准备释放锁
线程2执行完了!
线程1执行完了。
3、notify和notifyAll的区别
- 当你调用notify时,只有一个等待该锁的线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。
- 如果你调用notifyAll方法,那么等待该锁的所有线程都会被唤醒。唤醒并不意味着马上都可以同时执行,被唤醒的线程,还会去竞争这把锁,先获取到锁的先执行。
代码示例:
public class WaitNotifyAll implements Runnable {
private static final Object LOCK = new Object();
@Override
public void run() {
synchronized (LOCK) {
System.out.println(Thread.currentThread().getName() + " got lock!");
try {
System.out.println(Thread.currentThread().getName() + " Will enter waiting!");
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " Wake up, over..");
}
}
public static void main(String[] args) throws InterruptedException {
WaitNotifyAll r = new WaitNotifyAll();
Thread a = new Thread(r);
a.setName("线程A");
Thread b = new Thread(r);
b.setName("线程B");
Thread c = new Thread(new Runnable() {
@Override
public void run() {
synchronized (LOCK) {
System.out.println(Thread.currentThread().getName() + " notifyAll thread!");
// LOCK.notifyAll();
LOCK.notify();
}
}
});
c.setName("线程C");
a.start();
b.start();
Thread.sleep(1000);
c.start();
}
}
打印结果
线程A got lock!
线程A Will enter waiting!
线程B got lock!
线程B Will enter waiting!
线程C notifyAll thread!
线程A Wake up, over..
4、wait只会释放当前的那把锁
public class WaitNotifyReleaseOwnMonitor implements Runnable {
private static final Object LOCK1 = new Object();
private static final Object LOCK2 = new Object();
@Override
public void run() {
synchronized (LOCK1) {
System.out.println(Thread.currentThread().getName() + " GOT LOCK1 ");
synchronized (LOCK2) {
System.out.println(Thread.currentThread().getName() + " GOT LOCK2 ");
System.out.println(Thread.currentThread().getName() + " Release LOCK1 ! ");
try {
LOCK1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
WaitNotifyReleaseOwnMonitor r = new WaitNotifyReleaseOwnMonitor();
Thread thread = new Thread(r);
thread.setName("线程A");
thread.start();
Thread.sleep(1000L);
Thread b = new Thread(new Runnable() {
@Override
public void run() {
synchronized (LOCK1) {
System.out.println(Thread.currentThread().getName() + " GOT LOCK1 !");
System.out.println(Thread.currentThread().getName() + " TRY TO GOT LOCK2 !");
synchronized (LOCK2) {
System.out.println(Thread.currentThread().getName() + " GOT LOCK2 !");
}
}
}
});
b.setName("线程B");
b.start();
}
}
打印结果
线程A GOT LOCK1
线程A GOT LOCK2
线程A Release LOCK1 !
线程B GOT LOCK1 !
线程B TRY TO GOT LOCK2 !
5、wait、notify、notifyAll特点,性质整理
- 使用的话必须先拥有monitor锁
- 使用notify,只会唤醒其中一个持有等待该锁的线程。
- 属于Object类
- 同时持有多个锁的话,wait只会释放,对应的那个对象的那把锁。
6、wait原理图示
- 1是线程准备好可以为它分配资源
- 2、5都是准备抢紫色释放的锁
- 紫色的球释放锁有两种途径,①:调用wait释放monitor ②:自然执行完退出
- 4被唤醒之后会变成5同2一样去抢锁,抢到就可以执行自己的逻辑了。
三、wait、notify、notifyAll常见的面试问题
1、不准使用阻塞队列,只用wait和notify实现一个生产者和消费者模型
public class ProducterConsumerModel {
public static void main(String[] args) {
EventStoreage storeage = new EventStoreage();
Producer producer = new Producer(storeage);
Consumer consumer = new Consumer(storeage);
Thread a = new Thread(producer);
a.setName("生产者");
Thread b = new Thread(consumer);
b.setName("消费者");
a.start();
b.start();
}
}
/**
* 生产者
*/
class Producer implements Runnable {
private EventStoreage storeage;
public Producer(EventStoreage storeage) {
this.storeage = storeage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storeage.put();
}
}
}
class EventStoreage {
private int maxSize;
private LinkedList<Date> storeage;
public EventStoreage() {
this.maxSize = 10;
this.storeage = new LinkedList<>();
}
public synchronized void put() {
//我们调用仓库的放入方法的时候,先判断当前仓库已经储存的数量,是否达到了最大的数量
//如果达到了最大的数量,我们就释放当前锁对象 即:EventStoreage,
// 然后进入blocked状态,等待消费者将生产唤醒。
while (maxSize == storeage.size()) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果我们并没生产到仓库可以储存的最大数量,我们就往仓库里面放
storeage.add(new Date());
System.out.println("仓库里面已经有了" + storeage.size() + "个产品。");
//放一次我们就唤醒当前在等待该对象monitor的线程
this.notify();
}
public synchronized void take() {
//我们消费的时候,先判断当前仓库是否已经被我们消费完了,
// 如果消费完了,我们就放弃当前的对象锁,让生产者获取到锁,往仓库里放。
while (storeage.size() == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了" + storeage.poll() + ",现在仓库还剩下:" + storeage.size());
//只要消费一次我们就唤醒在等待该对象锁的线程
//唤醒对象:可能是陷入阻塞的生产者,也可能是陷入阻塞的消费者
this.notify();
}
}
/**
* 消费者
*/
class Consumer implements Runnable {
private EventStoreage storeage;
public Consumer(EventStoreage storeage) {
this.storeage = storeage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storeage.take();
}
}
}
2、两个线程交替打印0~100的奇偶数,要求:偶线程打印0,奇线程打印1,如此循环往复
2.1、使用synchronized实现
public class WaitNotifyPrintOddEvenSync {
private static int count;
private static final Object LOCK = new Object();
public static void main(String[] args) {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
while (count < 100) {
synchronized (LOCK) {
//偶数
if ((count & 1) == 0) {
System.out.println(Thread.currentThread().getName() + "打印出:" + count);
count++;
}
}
}
}
}, "偶数线程");
Thread b = new Thread(new Runnable() {
@Override
public void run() {
while (count < 100) {
synchronized (LOCK) {
//奇数
if ((count & 1) == 1) {
System.out.println(Thread.currentThread().getName() + "打印出:" + count);
count++;
}
}
}
}
}, "奇数线程");
b.start();
a.start();
}
}
2.2、使用wait、notify实现
public class WaitNotifyPrintOddEvenWait {
private static int count;
private static final Object LOCK = new Object();
static class TurningRunner implements Runnable {
@Override
public void run() {
//count =100的时候
while (count <= 100) {
//阻塞
synchronized (LOCK) {
System.out.println(Thread.currentThread().getName() + "打印出:" + count);
count++;
//唤醒99,但是99还得不到执行
LOCK.notify();
//count = 101,于是跳过wait
if (count <= 100) {
try {
//释放 LOCK
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//虽然count = 101跳过了wait,但是此时线程正常执行完,会释放monitor,于是99wait会继续往下执行,然后也释放monitor,程序正常退出
}
}
}
}
public static void main(String[] args) {
TurningRunner r = new TurningRunner();
Thread a = new Thread(r, "偶数");
Thread b = new Thread(r, "奇数");
a.start();
b.start();
}
}
3、为什么wait需要在同步代码块里面使用,而sleep不需要?
主要是为了使通信更加可靠,防止死锁和永久等待的发生,假如没有同步代码块的保护,线程直接切换把所有的notify都执行了,那么剩下的wait就只有永远等待了。
4、为什么线程通信的方法wait、notify、notifyAll定义在Object类里面,而sleep定义在Thread里面?
因为wait、notify、notifyAll是一个锁级别的操作,而锁其实是和对象绑定的,但是一个线程可以有多个锁,并且锁之间是相互配合的,如果定义在thread里面就没法灵活的定义了。
5、wait是属于object对象,那么调用thread.wait会怎么样?
thread在jvm退出的时候会自动的notify,如果用thread去定义wait等操作的话,会打乱我们的设计逻辑。
6、如何选择notify还是notifyAll
区别就在于你想唤醒多个还是一个线程。
四、sleep方法详解
1、sleep的作用
- 可以让线程在预期的时间执行,在调用sleep期间不再占用CPU的资源。
- sleep不会释放锁,包括synchronized和lock。
2、sleep方法响应中断
- sleep可以响应中断,并且抛出InterruptedException异常
- 清除中断状态
3、sleep的特点
- sleep可以让线程进入timed waiting状态,并且不占用cpu的资源,但是不释放锁,知道规定的时间后再执行,休眠期间如果被中断,会抛出异常并且清除中断状态。
4、sleep和wait的区别?
相同点:
- wait和sleep都可以使线程阻塞,对应线程的状态是waiting或则timed waiting
- wait和sleep都可以响应中断信号,即:thread.interrupt()
不同点:
- wait方法必须在synchronized修饰的方法或则代码块中才能执行,而sleep是不需要的。
- 在同步的方法里面执行sleep方法的时候,不会释放monitor锁,但是wait方法是要释放monitor锁的。
- sleep方法在短暂的休眠之后,会主动的退出阻塞状态,而没有指定时间的wait方法,需要其他持有该对象monitor的线程唤醒该线程,或则被其他线程中断才能退出阻塞。
- wait()和notify()、notifyAll()是object类的方法,sleep()和yield是thread类的方法。
五、jion方法详解
1、作用
因为新的线程加入了我们这些线程,我们需要等待他执行完在,我们再执行。
2、用法
2.1、join的基本用法
public class Join {
/**
* 展示join的基本用法。主线程会去等待子线程执行完毕再往下运行。
*
* @param args
*/
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕。");
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕。");
}
});
thread.start();
thread1.start();
System.out.println("开始等待子线程运行完毕。");
try {
thread.join();
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("所有的子线程已经执行完毕了");
}
}
2.2、join被打断之后
@Slf4j
public class JoinInterrupt {
//join遇到中断的情况:子线程去中断主线程,主线程收到中断的信号,主线程抛异常停止,然后通知子线程也停止
public static void main(String[] args) {
//主线程对象
Thread mainThread = Thread.currentThread();
//子线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
mainThread.interrupt();
try {
//子线程sleep过程可以收到主线程的中断信号
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
log.error("子线程收到主线程传来的中断信息,并且自己马上进行中断操作。");
throw new RuntimeException(e);
}
log.error("子线程并没有被中断!");
}
});
//启动子线程
thread.start();
try {
//主线程等待子线程执行完毕,由于子线程发出了中断主线程的信号
thread.join();
} catch (InterruptedException e) {
//如果想要停止子线程,那么还应当通知子线程停止执行
thread.interrupt();
//主线程会抛出异常停止执行
log.error("主线程收到中断信息,并且进行中断操作。");
throw new RuntimeException(e);
}
log.error("主线程并没有被中断。。。。。。。");
}
}
2.3、主线程在等待子线程join的过程主线程的线程状态
@Slf4j
public class JoinStateThread {
// 主线程在等待子线程的时候属于线程的什么状态 ?
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
log.info("主线程在子线程进行sleep之前为:{}", mainThread.getState());
TimeUnit.SECONDS.sleep(3);
log.info("主线程在子线程进行sleep之后为:{}", mainThread.getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//子线程启动
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//子线程执行完了,这时候主线程状态?
log.info("{}",mainThread.getState());
}
}
3、join的源码分析
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;
}
}
}
- join最终就是调用了wait方法,但是在源码里面并没有看到什么时候去唤醒wait
- 原来在jvm层,可以观察到在每个thread执行完毕之后都会调用lock.notify_all的方法。
- 这也是我们之前不推荐使用 Thread thread = new Thread(r); thread.wait 来做逻辑处理的原因。因为thread自己内部会去调用
3.2、写等价代码来分析join的实际原理
@Slf4j
public class JoinPrinciple {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行完毕");
}
});
//子线程启动
thread.start();
System.out.println("开始等待子线程运行完毕。");
// thread.join();
//下面的三行代码 等价于 thread.join()
synchronized (thread) {
thread.wait();
}
System.out.println("所有的子线程运行完毕。");
}
}
六、yield方法详解
1、含义
- 释放该线程的CPU时间片
- 比如有个线程被调度到了,但是目前没有满足继续执行的条件,那么就把这个CPU资源让给别的线程。
- 释放了CPU时间片之后,该线程的线程状态仍然是runnable而不是blocked、或则wait,只是释放CPU资源,而不释放自己的monitor,下一次的CPU调度依然可能把它调度起来。
2、定位
- JVM不保证会遵循yield的规则
- 比如说CPU资源很充足,就算该线程调用了yield,JVM也不会把你的CPU资源给释放掉,这个实现逻辑不通的JVM实现也不一样。
3、yield和sleep的区别
- sleep和yield都不会释放monitor,并且交出cpu资源。
- sleep不能像yield一样交出cpu资源之后马上就被调度,线程调用sleep之后,线程状态为timed waiting,从timed waiting到runnable是需要等到时间结束的,而yield交出cpu资源之后当前的线程状态仍然是runnable的。
七、说明
本文为学习慕课网悟空老师的课程《线程八大核心+Java并发底层原理精讲》笔记,文章为原创,知识点为老师所讲,有兴趣可以购买悟空老师的课程学习,支持知识付费,笔记如果有误希望大家指正,谢谢。