JAVA多线程基础篇-线程通信(wait/notify)

1.概述

在JAVA多线程中,线程通信是重要概念之一。线程通信能够使系统之间的交互性更强大,在大大提高CPU利用率的同时还会使程序员对各线程在任务处理过程中进行有效把控。本文将针对使用wait/notify进行线程之间通信进行分析,详述其实现过程、原理以及相关注意事项。

2.等待/通知机制的实现

2.1 线程通信概念

多个线程处理同一资源,但处理的动作不相同(线程执行的任务不同)。

2.2 为什么要进行线程通信

多个线程并发执行,默认情况下CPU是随机切换线程,当需要多个线程来共同完成一件事情,并且有规律地进行,此时线程之间则需要进行通信,协助完成数据的读取与处理。

2.3 什么是等待唤醒机制

指的是一个线程A调用了对象object的wait()方法进入等待状态,另一个线程B调用了对象object的notify()或者notifyAll()方法,线程A接收到通知后从对象object的wait()方法返回,继续执行后续操作。线程A和线程B通过对象object来完成交互,而对象上的wait()和notify/notifyAll()像一个开关信号,用来完成等待者和处理者之间的交互工作。

2.4 等待唤醒机制的实现

2.4.1 等待唤醒机制实现方法

当多个线程处理同一个资源,并且任务不同时,需要进程通信来帮助解决线程之间对同一个变量的使用和操作。等待唤醒机制其实是多个线程间的一种协作机制,主要用于解决线程之间的通信问题,主要使用的方法有如下3个:
(1)wait:线程不再进行活动,不再参与调度,进入WAITING状态,因此不会浪费CPU资源,也不会去竞争锁资源。它需要等待其它线程执行一个特殊动作,也就是“notify(通知)”在这个对象上等待的线程从WAITING状态中释放出来,重新进入调度队列(ready queue)中。
(2)notify:则选取所通知对象的 wait set 中的一个线程释放;
(3)notifyAll:则释放所通知对象的 wait set 上的全部线程。

wait/notify方法:

方法 说明
wait() 当前线程被阻塞,线程进入 WAITING 状态
wait(long) 设置线程阻塞时长,线程会进入 TIMED_WAITING 状态。如果设置时间内(毫秒)没有通知,则超时返回
wait(long, int) 纳秒级别的线程阻塞时长设置
notify() 通知同一个对象上已执行 wait() 方法且获得对象锁的等待线程
wait() 当前线程被阻塞,线程进入 WAITING 状态
notifyAll() 通知同一对象上所有等待的线程

2.4.2 注意事项

哪怕只通知了一个等待的线程,被通知的线程也不能立即恢复执行,因为它当初中断的地方在同步代码块内,而此刻它已经不再持有锁,所以需要获取锁(与其它线程进行资源竞争),成功之后才能在当初调用wait方法之后的地方恢复执行。

2.4.3 细节

1.wait方法与notify方法必须要由同一个锁对象调用。因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
2.wait方法与notify方法是属于Object类方法的。因为锁对象可以是任意对象,而任意对象的所属类都继承了Object类。
3.wait方法与notify方法必须要在同步代码块或者是同步函数中使用,因为必须通过锁对象调用者两个方法。

2.4.4 线程状态切换

(1)如果能获取锁,线程将从WAITING状态变成RUNNABLE状态;
(2)若获取不到锁,则线程又会从WAITING状态变成BLOCKED状态。

2.5 线程状态

线程状态 导致状态产生的条件
NEW(新建) NEW(新建) 线程刚刚被创建,但是还未启动,还没调用start方法
RUNNABLE(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己的代码,也可能未运行
BLOCKED(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其它线程所持有,则该线程进入Blocked状态;当该线程持有锁时,该线程变成Runnable状态
WAITING(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作,则该线程进入Waiting状态,进入此状态则不能自己唤醒,需要等待另一个线程调用notify或者notifyAll方法才能唤醒
TIMED WAITING(计时等待) 与waiting状态类似,有几个方法有超时参数,调用他们将进入Timed Waiting状态,这一状态将一直保持到超时期满或者收到唤醒通知。带有超时参数的常用方法有Thread.sleep,Object.wait
TEMINATED(被终止) TEMINATED(被终止) 因为run方法正常退出而死亡,或者由于异常未被捕获终止了run方法而死亡

2.5.1 锁阻塞状态

JAVA多线程基础篇-线程通信(wait/notify)_第1张图片

2.5.2 Timed Waiting(计时等待)

JAVA多线程基础篇-线程通信(wait/notify)_第2张图片

2.5.3 Waiting状态图

JAVA多线程基础篇-线程通信(wait/notify)_第3张图片

2.5.4 各个状态之间转换

JAVA多线程基础篇-线程通信(wait/notify)_第4张图片

3.案例

3.1 生产者/消费者案例

案例说明:蛋糕房生产蛋糕,消费者消费蛋糕,当没有蛋糕了(蛋糕状态为false),消费者等待,生产者线程生产蛋糕(蛋糕状态为true),并通知消费者消费蛋糕(解除消费者等待状态),此时生产者进入等待状态(已经生产过了),接下来,消费者能否进一步执行则取决于锁的获取情况,如果消费者获取到锁,就执行消费动作,消费完成(将蛋糕状态改为false),则通知生产者线程生产(解除生产者的等待状态),消费者线程进入等待,生产者能否进一步执行取决于锁的获取情况。
生产者ProducerThread:

@Slf4j
public class ProducerThread implements Runnable {

    private Cake cake;

    public ProducerThread(Cake cake) {
        this.cake = cake;
    }

    @Override
    public void run() {
        synchronized (cake) {
            if (cake.isFlag()) {
                try {
                    cake.wait();
                    log.info("正在生产蛋糕,请稍等。。。");
                } catch (InterruptedException e) {
                    log.error("休眠异常:{}", e);
                }
            }
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cake.setFlag(true);
            log.info("蛋糕生产完成。。。");
            cake.notify();
        }
    }
}

消费者代码:

@Slf4j
public class ConsumerThread implements Runnable {

    private Cake cake;

    public ConsumerThread(Cake cake) {
        this.cake = cake;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (cake) {
                if (!cake.isFlag()) {
                    try {
                        cake.wait();
                    } catch (InterruptedException e) {
                        log.error("线程休眠异常:{}", e);
                    }
                }
                log.info("正在消费蛋糕!");
                cake.setFlag(false);
                log.info("蛋糕消费完成!!!");
                cake.notify();
            }
        }
    }
}

测试代码:

public static void main(String[] args) {
        Cake cake = new Cake();
        cake.setFlag(true);
        
        ConsumerThread consumerThread = new ConsumerThread(cake);
        new Thread(consumerThread).start();

        ProducerThread producerThread = new ProducerThread(cake);
        new Thread(producerThread).start();
    }

运行结果如下:
JAVA多线程基础篇-线程通信(wait/notify)_第5张图片

3.2 方法wait()锁释放与notify()锁不释放

当方法wait()被执行后,锁被自动释放,但执行完notify()方法,锁却不自动释放。

3.2.1 wait()调用后,锁被释放

public class LockDemo {

    private Object object;

    public LockDemo(Object object) {
        this.object = object;
    }

    public void testMethod() {
        synchronized (object) {
            try {
                System.out.println("begin wait()");
                object.wait();
                System.out.println("end wait()");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadA extends Thread {

    private Object object;

    public ThreadA(Object object) {
        this.object = object;
    }

    public void run() {
        LockDemo lockDemo = new LockDemo(object);
        lockDemo.testMethod();
    }

}
public class ThreadB extends Thread {

    private Object object;

    public ThreadB(Object object) {
        this.object = object;
    }

    public void run() {
        LockDemo lockDemo = new LockDemo(object);
        lockDemo.testMethod();
    }

}

主函数代码如下:

public class Test {
    public static void main(String[] args) {

        Object object = new Object();

        new ThreadA(object).start();

        new ThreadB(object).start();
    }
}

运行结果如下:
JAVA多线程基础篇-线程通信(wait/notify)_第6张图片
由上图可知,wait()方法执行后会释放锁。

3.2.2 notify()执行后,锁不释放

public class NotifyUnLock {

    private Object object;

    public NotifyUnLock(Object object) {
        this.object = object;
    }

    public void testMethod() {
        try {
            synchronized (object) {
                System.out.println("begin wait() ThreadName=" + Thread.currentThread().getName() + "  time=" + System.currentTimeMillis());
                object.wait();
                System.out.println("end wait() ThreadName = " + Thread.currentThread().getName() + "  time=" + System.currentTimeMillis());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void syncNotifyMethod(Object object) {
        try {
            synchronized (object) {
                System.out.println("begin notify() ThreadName=" + Thread.currentThread().getName() + "  time=" + System.currentTimeMillis());
                object.notify();
                System.out.println("end notify() ThreadName=" + Thread.currentThread().getName() + "  time=" + System.currentTimeMillis());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class ThreadA extends Thread {
    private Object object;

    public ThreadA(Object object) {
        this.object = object;
    }

    public void run() {
        NotifyUnLock notifyUnLock = new NotifyUnLock(object);
        notifyUnLock.testMethod();
    }
}
public class NotifyThreadA extends Thread {
    private Object object;

    public NotifyThreadA(Object object) {
        this.object = object;
    }

    public void run() {
        NotifyUnLock notifyUnLock = new NotifyUnLock(object);
        notifyUnLock.syncNotifyMethod(object);
    }
}
public class NotifyThreadB extends Thread {
    private Object object;

    public NotifyThreadB(Object object) {
        this.object = object;
    }

    public void run() {
        NotifyUnLock notifyUnLock = new NotifyUnLock(object);
        notifyUnLock.syncNotifyMethod(object);
    }
}

测试代码如下:

public class Test {
    public static void main(String[] args) {
        Object object = new Object();

        ThreadA threadA = new ThreadA(object);
        threadA.start();

        NotifyThreadA notifyThreadA = new NotifyThreadA(object);
        notifyThreadA.start();

        NotifyThreadB notifyThreadB = new NotifyThreadB(object);
        notifyThreadB.start();
    }
}

运行结果为:
JAVA多线程基础篇-线程通信(wait/notify)_第7张图片
由多次执行结果可知,两个线程的notify方法总是异步执行,存在明显先后关系。在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。wait和notify要严格控制执行顺序,如果先通知了,wait方法也没必要执行了,会一直处于waiting状态。

3.2.3 多生产与多消费:线程假死

“假死”的现象其实就是线程进入WAITING等待状态。如果全部线程都进入WAITING状态,则程序就不再执行任何业务功能了,整个项目可能呈停止状态。
测试代码如下:

@Slf4j
public class MultiConsumer {

    private Object object;

    public MultiConsumer(Object object) {
        this.object = object;
    }

    public void getValue() {
        try {
            synchronized (object) {
                while (ListQuere.value.equals("")) {
                    System.out.println("消费者" + Thread.currentThread().getName() + "START WAITING!");
                    object.wait();
                }
                System.out.println("消费者" + Thread.currentThread().getName() + "START RUNNABLE");
                System.out.println("消费者获取数据为:" + ListQuere.value);
                ListQuere.value = "";
                object.notify();
            }
        } catch (Exception e) {
            log.error("exception:{}", e);
        }
    }
}

@Slf4j
public class MultiProducer {

    private Object object;

    public MultiProducer(Object object) {
        this.object = object;
    }

    public void setObject() {
        try {
            synchronized (object) {
                while (!ListQuere.value.equals("")) {
                    System.out.println("生产者" + Thread.currentThread().getName() + "start waiting +++");
                    object.wait();
                }
                System.out.println("生产者" + Thread.currentThread().getName() + "start running +++");
                String value = System.currentTimeMillis() + "_";
                ListQuere.value = value;
                object.notify();
            }

        } catch (Exception e) {
            log.error("exception:{}", e);
        }
    }
}

public class ThreadC extends Thread{

    private MultiConsumer consumer;

    public ThreadC(MultiConsumer consumer) {
        this.consumer = consumer;
    }

    public void run() {
        while (true) {
            consumer.getValue();
        }
    }
}

public class ThreadP extends Thread {

    private MultiProducer producer;

    public ThreadP(MultiProducer producer) {
        this.producer = producer;
    }

    public void run() {
        while (true) {
            producer.setObject();
        }
    }

}


public class ListQuere {

    public static String value = "";

}

public class Test {

    public static void main(String[] args) {
        Object lock = new Object();
        MultiProducer producer = new MultiProducer(lock);
        MultiConsumer consumer = new MultiConsumer(lock);
        ThreadP[] threadPS = new ThreadP[2];
        ThreadC[] threadCS = new ThreadC[2];
        for (int i = 0; i < 2; i++) {
            threadPS[i] = new ThreadP(producer);
            threadPS[i].setName("生产者" + (i + 1));
            threadCS[i] = new ThreadC(consumer);
            threadCS[i].setName("生产者" + (i + 1));
            threadPS[i].start();
            threadCS[i].start();
        }
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread[] threads = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
        Thread.currentThread().getThreadGroup().enumerate(threads);
        for (int i = 0; i < threads.length; i++) {
            System.out.println(threads[i].getName() + " " + threads[i].getName() + "state:" + threads[i].getState());
        }

    }
}

测试结果如下:
JAVA多线程基础篇-线程通信(wait/notify)_第8张图片
假死出现的原因有可能是连续唤醒同类。解决这种问题的方法,不仅需要唤醒同类,也需要唤醒异类。解决上述问题中的方法:将MultiProducer和MultiConsumer中的notify()方法改为notifyAll()方法,它的原理不仅通知同类线程,也通知异类,就不至于出现假死状态,程序也会一直运行。

3.3 为什么wait/notify方法要配合synchronized使用

wait和notify用来实现多线程之间的协调,wait表示让线程进入到阻塞状态,notify表示让阻塞的线程唤醒。wait和notify必然是成对出现的,如果一个线程被wait()方法阻塞,那么必然需要另外一个线程通过notify()方法来唤醒这个被阻塞的线程,从而实现多线程之间的通信。

要了解wait和notify为什么需要配合synchronized,首先需要了解synchronized底层原理,可查看我上一篇文章:JAVA多线程基础篇-关键字synchronized。在重量级锁状态下,对象头指针会指向ObjectMonitor对象,wait/notify也是objectMonitor对象的方法,monitor对象包含owner,waitSet,entryList和cxq等部分,这些部分的操作都必须由锁的持有线程或jvm本身来实现,比如wait方法的意思就是将本线程置入waitSet并释放锁,notify的意思就是把某个在waitSet中的线程放入entryList或cxq队列并唤醒。不管是哪个方法,都要求执行的线程为锁的持有线程。因此,如果不把两个方法写在同步代码块中,在编译期间就会提示错误。

4.小结

1.wait/notify是实现JAVA线程通信的一种方式,还可以通过管道流的方式来实现线程通信;
2.wait/notify方法的调用必须处在该对象的锁(Monitor)中,且wait、notify必须成对出现;
3.在示例中没有体现但很重要的是,wait/notify方法的调用必须处在该对象的锁(Monitor)中,也即,在调用这些方法时首先需要获得该对象的锁。否则会抛出IllegalMonitorStateException异常。

5.参考文献

1.《JAVA多线程编程核心技术》-高洪岩著
2.https://www.bilibili.com/video/BV1xr4y1p7w6?spm_id_from=333.337
3.https://segmentfault.com/a/1190000041800866

你可能感兴趣的:(多线程,java)