前面几篇算是总结了一些Java线程中比较模棱两可的内容,在线程状态一篇博文中,我们在介绍线程状态切换的图中时,出现了notify,wait等方法,这些是Object的方法。今天这篇博客就是针对这些方法做一些梳理,当然,不是全部的Thread和Object类中的方法介绍,而是只是总结一下关于多线程相关的方法。其实在实际开发写代码的时候,很少会直接接触这些方法,但是这些方法确实很多并发工具的底层原理,因此还是有必要总结一下。
Object类中的方法 简述 wait在某些场景,在线程执行的时候,在某些条件下,我们想让某些线程先休息一下(比如生产者消费者模式中,数据容器数据已经满了,这个时候就生产者就需要休息一下),等到某个时刻再被唤醒。这就是wait的作用,在上一篇博客的总结中,可以看到调用wait方法之后,线程进入到WAIT状态。而与之对应的,唤醒线程的方法就是notify或者notifyAll。wait是属于Object中的方法,通过对象调用wait方法的代码只能存在于同步代码块中。
进入到WAIT状态的线程,在以下4种场景下才会被唤醒
1、另一个线程调用这个对象的notify方法,且刚好被唤醒的就是本线程
2、另一个线程调用了notifyAll方法
3、本线程过了wait设置的超时时间
4、线程自身被通知中断(调用interrupt方法)
notify与wait相对应的就是notify和notifyAll方法,notify是随机唤醒一个对应对象的等待线程。而notifyAll是唤醒指定对象的所有等待线程。
notifyAllnotifyAll上面已经介绍,唤醒对象等待队列中的所有线程
实例 1、简单的wait和notify实例
public class WaitNotifyDemo {
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();//wait会释放锁
} catch (InterruptedException e) {//线程在wait的时候,可以响应中断,响应中断之后,线程就退出WAIT状态了
e.printStackTrace();
}
//线程从wait获得锁之后,继续从wait之后开始执行,而不是从开头开始执行
System.out.println(Thread.currentThread().getName()+"线程继续开始执行");
}
}
}
static class Thread2 extends Thread{
@Override
public void run() {
synchronized (object){
object.notify();
//线程2虽然执行了notify,但是并不会立刻释放这把锁,而是执行完synchronize代码块之后,才释放这把锁
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);//保证wait在notify之前执行
thread2.start();
}
}
2、简单的wait与notifyAll的实例
public class WaitNotifyAllDemo implements Runnable{
private static final Object resourceA = new Object();
@Override
public void run() {
synchronized (resourceA){
System.out.println(Thread.currentThread().getName()+"获取到资源锁");
try {
System.out.println("线程即将wait "+Thread.currentThread().getName());
resourceA.wait();//wait释放锁
System.out.println("线程"+Thread.currentThread().getName()+"被唤醒继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new WaitNotifyAllDemo();
Thread thread01 = new Thread(runnable);
Thread thread02 = new Thread(runnable);
Thread thread03 = new Thread(()->{
synchronized (resourceA){
resourceA.notifyAll();
System.out.println(Thread.currentThread().getName()+"notify all thread");
}
});
thread01.start();
thread02.start();
Thread.sleep(500);//保证线程1和线程2都进入WAIT状态
thread03.start();
}
}
3、wait方法是会释放锁的
public class WaitNotifyReleaseOwnMonitor {
//需要准备两把锁
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
synchronized (resourceA){
System.out.println(Thread.currentThread().getName()+" get resourceA lock");
synchronized (resourceB){
System.out.println(Thread.currentThread().getName()+" get resourceB lock");
try {
System.out.println(Thread.currentThread().getName()+ "release resourceA lock");
resourceA.wait();//线程进入wait会释放锁,但是只是释放其拥有的对象的锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread thread2 = new Thread(()->{
//等待一下,确保线程1进入wait状态
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceA){
System.out.println(Thread.currentThread().getName()+" other thread get resourceA lock");
System.out.println(Thread.currentThread().getName()+" other thread try to get resourceB lock");
synchronized (resourceB){//这个线程无法打印这行日志,因为wait只是释放指定对象的锁,并不会释放线程持有的所有锁
System.out.println(Thread.currentThread().getName()+" other thread get resourceB lock");
}
}
});
thread1.start();
thread2.start();
}
}
运行结果:
线程2,无法获得resourceB对象的锁,但是获得了resoureceA对象的锁,其实可以说明wait方法是释放锁的,但是只能释放指定对象的锁。
小结关于wait,notify,notifyAll方法,这里做一个小结。
1、wait,notify,notifyAll,必须在synchronize修饰的代码块或者方法中运行
2、notify只能唤醒一个等待线程,这个是随机的。
3、notifyAll可以唤醒全部
这些方法都属于Object,类似的功能有Condition锁,这个我们后面会总结,同时需要注意的是,如果一个线程同时持有多把锁,则线程调用wait方法进入等待的时候,只会释放wait对象对应的那把锁,这个时候就需要注意一下锁的释放顺序。同时被重新唤醒的线程,并不会立刻进入到RUNNABLE状态,而是和其他线程一样,也需要先进入到等待队列,然后获取锁,如果没有获取到锁,依旧是BLOCKED状态(没有获取到synchronize锁的状态)。如果WAIT期间,发生异常,则线程也可以直接进入到TERMINATED状态。
关于wait的原理,可以一张图总结一下
这张图应该还算比较经典了,将抢锁与wait等待的过程都表述的很详细了。
Thread类中的方法 sleep方法sleep与wait方法的差异,是面试中几乎都会问道的问题,但是sleep有着自己的特点。sleep方法可以让线程进入Waiting状态,并且不占用CPU资源,但是,sleep不会释放锁,直到规定时间后再执行,休眠期间sleep方法是可以响应中断的。
相比于直接用Thread.sleep(1000)这种方式,其实更推荐用TimeUnit.SECONDS.sleep(1),关于sleep的简单实例如下
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();
}
}
wait/notify与sleep的异同
1、两者都会让线程进入阻塞,也都能响应中断
2、不同的是wait只能用在同步方法中,而sleep不需要;wait会释放锁,而sleep不会;所属的类不同,sleep方法属于Thread类;sleep必须接受参数,wait是可以不传递参数的,wait是直到被唤醒。
join方法某个线程加入到当前线程的流程中,需要等待加入的线程运行完成之后,在操作其他的。
实例代码
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"执行完毕");
});
Thread thread2 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"执行完毕");
});
thread1.start();
thread2.start();
System.out.println("主线程开始等待子线程运行完毕");
//这里的join由子线程的对象进行调用,但含义实质是将子线程加入到主线程中
thread1.join();//含义:子线程加入到主线程中,主线程需要等待子线程执行完毕之后再运行
thread2.join();
System.out.println("所有子线程执行完毕,下面主线程可以运行了");
Thread.sleep(500);
System.out.println("主线程执行完毕");
}
}
有join的运行结果
如果将thread1.join()和thread2.join()两行代码注释掉,则会有如下输出结果
需要理解一下的是,虽然join方法是被子线程调用,但是实际是主线程被join,毕竟是主线程等待子线程运行完毕
join期间的中断
public class JoinInterrupt {
public static void main(String[] args) {
//拿到主线程的引用
Thread mainThread = Thread.currentThread();
Thread thread = new Thread(() -> {
try {
//在子线程中中断主线程
mainThread.interrupt();
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName()+"运行完毕");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"线程中断");
e.printStackTrace();
}
});
thread.start();
System.out.println("开始等待子线程运行");
try {
thread.join();//含义:thread加入到主线程中,主线程等待thread执行完成
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"线程被中断了");//主线程被中断了
e.printStackTrace();
}
System.out.println("主线程等待子线程执行完毕");
}
}
上述代码在子线程中中断了主线程,而这个时候,主线程正在join,因此会响应中断,但是会有一些问题,具体运行图如下所示
可以看到,主线程虽然被中断了,也正常响应了中断,但是这个时候子线程依旧在运行。在一定的业务场景下,这种是很危险的。因此,为了规范化一点,如果用到了join方法(虽然实际开发中也用的不多,毕竟有些文档也不建议我们直接使用JDK底层的多线程方法)在响应中断的时候,也要将这个中断信号传递给子线程,上述代码中针对join部分的处理需要改成如下代码
try {
thread.join();//含义:thread加入到主线程中,主线程等待thread执行完成
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"线程被中断了");//主线程被中断了
e.printStackTrace();
thread.interrupt();//如果主线程在join期间中断,规范的操作是将这个中断传递给子线程
}
主要是第6行代码
join期间的状态
public class JoinThreadState {
public static void main(String[] args) throws InterruptedException {
Thread mainThread = Thread.currentThread();
Thread thread = new Thread(() -> {
try {
Thread.sleep(3000);
System.out.println("主线程join期间的状态:" + mainThread.getState());//这里输出的是主线程join时候的状态
System.out.println(Thread.currentThread().getName() + "运行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
System.out.println("主线程等待子线运行完毕");
thread.join();
System.out.println("主线程运行完毕");
}
}
用代码说话——join期间,主线程的状态是WAITING,不是一些资料上说的BLOCKED
join原理join不是一个native的方法,其源码如下
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方法中的注释内容,其中有一段话很重要**:由于线程运行结束的时候会调用notifyAll方法,因此不推荐写代码的时候直接通过Thread的对象调用wait,notify和notifyAll方法**。
可以看到join的底层其实就是调用的wait方法,上述的一段话已经解释了这里的wait是由谁唤醒的。网上查了一堆资料,在join的底层jvm源码中确实发现了调用notifyAll的痕迹
所有线程在运行结束的时候,会自动调用notifyAll方法,这也是java不推荐我们直接通过thread对象调用wait,notify和notifyAll方法的原因。基于此,我们可以用相关的代码替换join
public class JoinPrinciple {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"执行完毕");
});
thread1.start();
System.out.println("主线程开始等待子线程运行完毕");
//这里的join由子线程的对象进行调用,但含义实质是将子线程加入到主线程中
//thread1.join();//含义:子线程加入到主线程中,主线程需要等待子线程执行完毕之后再运行
//thread1.join的等价代码,主线程获取到thread1这个对象锁之后,进入等待
//最后被thread1的run方法执行结束之后,被唤醒
synchronized (thread1){
thread1.wait();
}
System.out.println("所有子线程执行完毕");
}
}
yield方法
yield方法是我们需要总结的最后一个方法,yield的方法就是释放已经占有的CPU时间片,但是,尴尬的是yield一般不被JVM遵循,只需要了解其作用即可,实际开发中也用的很少。yield与sleep方法的区别就是,yield释放的线程,可能随时被再次调度。
生产者消费者模式其实聊到wait和notify的方法,不得不说一下生产者和消费者设计模式,这个设计模式是解耦的最好体现
图中左边代表消费者,右边代码生产者,二者的通信是通过中间的FIFO的队列,中间的FIFO队列满了,生产者阻塞。中间的FIFO满了,消费者阻塞。这里我们用wait和notify手动实现以下这个设计模式
实例代码
public class ProducerConsumerModel {
public static void main(String[] args) {
EventStorage storage = new EventStorage();
Producer producer = new Producer(storage);
Consumer consumer = new Consumer(storage);
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 storage;
public EventStorage(){
maxSize = 10;
storage = new linkedList<>();
}
public synchronized void put(){
while(storage.size() == maxSize){//如果仓库已经满了,则循环等待,wait释放锁
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("仓库里有了"+storage.size()+"个产品");
notify();//通知消费者取元素,这里为了简单,只有一个消费者,因此直接notify
}
public synchronized void take(){
while(storage.size() == 0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了"+storage.poll()+",现在仓库还剩下:"+storage.size());
notify();//通知生产者,生产数据
}
}
一些面试问题
1、两个线程交替打印0~100的奇偶数?
通过synchronize关键是,可以通过如下代码实现
public class WaitNotifyPrintOddEvenSync {
private static int count;
private static final Object lock = new Object();
//建立两个线程,一个只处理偶数,一个只处理奇数(用位运算处理)
//线程完成自己工作的同时,还需要考虑其他线程的输出,这里用synchronize来通信
public static void main(String[] args) {
Thread thread01 = new Thread(() -> {
while(count<100){
synchronized (lock){
if((count&1) == 0){
System.out.println(Thread.currentThread().getName()+": 输出偶数"+(count++));
}
}
}
},"偶数");
Thread thread02 = new Thread(() -> {
while(count<100){
synchronized (lock){
if((count&1) != 0){
System.out.println(Thread.currentThread().getName()+": 输出奇数"+(count++));
}
}
}
},"奇数");
thread01.start();
thread02.start();
}
}
利用wait和notify可以通过如下代码实现
public class WaitNotifyPrintOddEveWait {
private static int count = 0;
private static final Object lock = new Object();
//1.拿到锁,我们就打印
//2.打印完,唤醒其他线程,自己休眠
static class TurningRunner implements Runnable{
@Override
public void run() {
while(count<=100){
synchronized (lock){
//拿到锁就直接打印
System.out.println(Thread.currentThread().getName()+":"+count++);
//打印完成之后,唤醒其他线程,然后自己休眠
lock.notify();
if(count<=100){
try {
//打印完了之后,count还是小于100,就自己休眠
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public static void main(String[] args) {
new Thread(new TurningRunner(),"偶数").start();
//Thread.sleep(100);可以休眠一下,防止奇偶错乱
new Thread(new TurningRunner(),"奇数").start();
}
}
2、手写消费者生产者设计模式?
上述相关内容已经有相关解答
3、wait为什么需要在同步代码块中使用,而sleep不需要?
答:为了让通信变得更加可靠,如果wait和notify不是放在同步代码块中,则很大程度上其执行顺序可以任意,毕竟线程的启动顺序也是随机的,这样很容易在某些线程wait了之后,无法得到notify,造成死锁或者长时间等待。而sleep则不同,只是简单的针对自己单独线程的
4、wait,notify,notifyAll为什么定义在Object中,而sleep定义在Thread中?
Java中的一个锁,其实就是一个对象,Java中一个线程中其实可以持有多把锁,而且这些锁直接可以相互配合,如果将wait,notify,notifyAll方法定义在Thread类中,则并不能实现多把锁互相配合的场景。同时本身wait和notify也是基于锁而定义的。而sleep是针对线程的,并不针对锁的操作,因此sleep可以定义在Thread中
5、如果调用Thread对象中的wait方法会怎么样?
这个在博客中已经总结,Java线程本身在运行完成的时候,会自动调用notify和notifyAll的方法,如果直接调用Thread对象的wait方法,可能会干扰到原有的线程通信逻辑。
总结关于stop,suspend,resume这些过时的方式,这里就不再总结了



