Java多线程拾遗(五)——notify和wait的一些事儿

前言

同样参考《Java高并发编程详解》一书这篇博客开始梳理notify和wait的一些事儿。

同步阻塞与异步非阻塞

我们之前总结过什么是同步,什么是阻塞。但并没有总结清楚同步阻塞与异步非阻塞的东西。《Java高并发编程详解》一书聊聊同步阻塞和异步非阻塞的区别,该书中通过一个实例说明了这两者的区别。

同步阻塞

实例中,具体的需求也很简单,就是客户端提交一个Event类型的请求,交给服务器,服务器处理相关的请求,然后将处理结果返回给客户端。同时我们可看到,在同步阻塞中。对于客户端而言,提交了请求之后,需要一直等待服务端返回结果,这个期间客户端无法做其他事情,只能等着服务端处理完相关数据,客户端收到结果之后才能进行下一步操作。所以对于客户端和服务端来说,这个请求是同步的,客户端和服务端都得等着一个请求结束才能进行下一步,无法做到异步

对于客户端来说,在没有收到服务端的返回结果的时候,无法操作下一步,也只能等着,这个状态称为阻塞中。因此这就是同步阻塞。可以看出,同步说的是客户端和服务器端在同一个时间必须同步完成某一件事情,而阻塞只是相对于客户端而言,两者还是有区别的。

Java多线程拾遗(五)——notify和wait的一些事儿_第1张图片

异步非阻塞

理解了同步阻塞,再来理解异步非阻塞,就容易的多了,所谓的异步,这里指的是客户端在发送请求之后,会立即受到服务端的一个返回。这个时候客户端可以处理其他事情,客户端的下一步操作并不依赖服务端的这个实时结果,如果后续客户端需要结果了,再根据工单号调用相关的查询接口获取结果。服务端与客户端处理数据的方式达到了异步,并不同步。这个时候,对于客户端来说,并不用等服务端的结果了,因此不会因为这个而阻塞,故而称为异步非阻塞。

Java多线程拾遗(五)——notify和wait的一些事儿_第2张图片

wait和notify

乞丐版

乞丐版本的生产者和消费者

@Slf4j
public class ProduceAndConsumerVersion01 {
    
    private int i = 1;
    private final Object LOCK=new Object();

	//生产者
    private void produce(){
        synchronized (LOCK){
            log.info("P->,{}",i++);
        }
    }

	//消费者
    private void consume(){
        synchronized (LOCK){
            log.info("C->,{}",i);
        }
    }

    public static void main(String[] args) {
        ProduceAndConsumerVersion01 pc = new ProduceAndConsumerVersion01();
		//启动生产者线程
        new Thread("P"){
            @Override
            public void run() {
                while(true){
                    pc.produce();
                }
            }
        }.start();
		//启动消费者线程
        new Thread("C"){
            @Override
            public void run() {
                while(true){
                    pc.consume();
                }
            }
        }.start();
    }
}

一个线程生产数据,一个线程消费数据,且操作的都是同一个变量,逻辑上看似乎没毛病,但是这个真就正常么?

运行之后看到如下结果。

Java多线程拾遗(五)——notify和wait的一些事儿_第3张图片

消费者永远只是消费到最新的数据。这里的根本原因就是生产者线程和消费者线程之间没有一个通信机制,消费者不知道生产者生产了数据,生产者不知道消费者消费了数据。notify和wait就是干这个的,就是线程间的一种通信方式。至少我们需要有一种方式,让消费者知道生产者生产了数据,生产者知道消费者消费了数据。

notify和wait的引入

/**
 * autor:liman
 * createtime:2020/6/14
 * comment:生产者和消费者,
 */
@Slf4j
public class ProduceAndConsumerVersion02 {

    private int i = 0;

    private final Object LOCK = new Object();

    //引入一个标记,表示是否生产了数据
    private volatile boolean isProduced = true;

    public void produce() {
        synchronized (LOCK) {
            //如果存在数据,则阻塞
            if (isProduced) {
                try {
                    LOCK.wait();
                } catch (InterruptedException e) {
                    log.error("InterruptedException:{}", e);
                }
            } else {//如果不存在数据,则需要生产数据
                i++;
                log.info("P->,{}", i);
                LOCK.notify();
                isProduced = true;
            }
        }
    }

    public void consume() {
        synchronized (LOCK) {
            //如果有数据,则需要消费数据
            if (isProduced) {
                log.info("C->,{}", i);
                LOCK.notify();
                isProduced = false;
            } else {//如果没有数据,则需要阻塞,等待生产者产生数据
                try {
                    LOCK.wait();
                } catch (InterruptedException e) {
                    log.error("InterruptedException:{}", e);
                }
            }
        }
    }

    public static void main(String[] args) {
        ProduceAndConsumerVersion02 pc = new ProduceAndConsumerVersion02();
        //启动一个生产者
        Stream.of("P1").forEach(n -> {
            new Thread(n) {
                @Override
                public void run() {
                    while (true) {
                        pc.produce();
                    }
                }
            }.start();
        });
		//启动一个消费者
        Stream.of("C1").forEach(n -> {
            new Thread(n) {
                @Override
                public void run() {
                    while (true) {
                        pc.consume();
                    }
                }
            }.start();
        });
    }
}

从运行结果来看,程序可以正常生产数据和消费数据。

Java多线程拾遗(五)——notify和wait的一些事儿_第4张图片

如果有多个消费者,用如下的代码,是不是正常的呢

package com.learn.thread.update.notifyandwait;

import lombok.extern.slf4j.Slf4j;

import java.util.stream.Stream;

/**
 * autor:liman
 * createtime:2020/6/14
 * comment:生产者和消费者,
 */
@Slf4j
public class ProduceAndConsumerVersion02 {

    private int i = 0;

    private final Object LOCK = new Object();

    //引入一个标记,表示是否生产了数据
    private volatile boolean isProduced = true;

    public void produce() {
        synchronized (LOCK) {
            //如果存在数据,则阻塞
            if (isProduced) {
                try {
                    log.info("生产者线程:{} 等待", Thread.currentThread().getName());
                    LOCK.wait();
                    log.info("生产者线程:{} 结束等待", Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    log.error("InterruptedException:{}", e);
                }
            }
            //如果不存在数据,则需要生产数据
            i++;
            log.info("{} produce -> {}", Thread.currentThread().getName(), i);
            LOCK.notifyAll();
            isProduced = true;
        }
    }

    public void consume() {
        synchronized (LOCK) {
            if (!isProduced) {//如果没有数据,则需要阻塞,等待生产者产生数据
                try {
                    log.info("消费者线程:{} 等待", Thread.currentThread().getName());
                    LOCK.wait();
                    log.info("消费者线程:{} 结束等待", Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    log.error("InterruptedException:{}", e);
                }
            }
            //否则直接消费数据
            log.info("{} consume -> {}", Thread.currentThread().getName(), i);
            LOCK.notifyAll();
            isProduced = false;
        }
    }

    public static void main(String[] args) {
        ProduceAndConsumerVersion02 pc = new ProduceAndConsumerVersion02();
        //这里构建多个生产者
        Stream.of("P1","P2").forEach(n -> {
            new Thread(n) {
                @Override
                public void run() {
                    while (true) {
                        pc.produce();
                    }
                }
            }.start();
        });
		//这里构建多个消费者
        Stream.of("C1","C2").forEach(n -> {
            new Thread(n) {
                @Override
                public void run() {
                    while (true) {
                        pc.consume();
                    }
                }
            }.start();
        });
    }
}

运行结果:

Java多线程拾遗(五)——notify和wait的一些事儿_第5张图片

发现程序卡住了,并没有顺利往下运行了,我们通过jstack命令查看到,发现我们创建的四个线程,四个都处于wait状态,每个线程都不知道下一步该干啥了,自然就无法正常运行了。其实解决方法也很简单,将消费者线程或者生产者线程中的notify改成notifyAll即可

为什么会出现上述现象?可以参考如下的线程执行轨迹,P——生产者,C——消费者

1. P1 产生了一个数字1。
2. P2 想继续产生数据,发现满了,在wait里面等了。
3. P3 想继续产生数字,发现满了,在 wait 里面等了。
4. C1 想来消费数字,C2,C3 就在 get 里面等着。
5. C1 开始执行,获取1,然后调用 notify 然后退出。
如果 C1 把 C2 唤醒了,所以P2 (其他的都得等)只能在put方法上等着。(等待获取synchoronized (this) 这个monitor)。
C2 检查 while 循环发现并没有任何数据,所以就在 wait 里面等着。
C3 也比 P2 先执行,那么发现依旧没有数据,只能等着了。

6. 这时候我们发现 P2、C2、C3 都在等着锁,最终 P2 拿到了锁,放一个 1,notify,然后退出。
7. P2 这个时候唤醒了P3,P3发现队已经存在数据了,没办法,只能等它变为空。
8. 这时候没有别的调用了,那么现在这三个线程(P3, C2,C3)就全部变成 suspend 了,都在哪儿等着

分析了一下这个执行轨迹,会发现其实根本原因就在于有多个消费者和生产者的情况下,如果单纯用notify,则会出现消费者或者生产者只是唤起了同类,就会出现上述情况,而notifyAll,就是唤起这个对象相关的所有处于wait状态的线程。

如果将上述代码改成notifyAll之后,多加几个消费者,然后运行稍微久一点,可以发现上述代码还有一个问题,出现多个消费者重复消费同一条数据。

Java多线程拾遗(五)——notify和wait的一些事儿_第6张图片

这是为啥?按照如下的运行轨迹来进行分析

1. C1 拿到了锁进入同步代码块。
2. C1 发现没有数据,然后进入等待,并释放锁 (wait方法是会释放锁的)。
3. 此时C2 拿到了锁,发现 依旧没有数据,然后进入等待,并释放锁 。
4. 这个时候有个线程P1往里面加了个数据1,那么notifyAll所有的等待的线程都被唤醒了。
5. C1,C2 重新获取锁,假设又是C1拿到了。然后他就走出if语句块,消费了一个数据,没有问题。
6. C1 移除数据后想通知别人,数据已经被消费了,于是调用了 notifyAll ,这个时候就把 C2 给唤醒了,那么 C2 接着往下走。
7. 这时候 C2 就出问题了,因为其实此时的竞态条件已经不满足了 (数据已经被消费了)。C2 以为还可以从if语句正常出来,然后执行之后的语句,结果就重复消费了。

notifyAll虽然解决了所有线程等待的问题,但是如果不是在while中循环判断,会出现重复消费的问题,因此正常的逻辑我们应该采用如下的代码

/**
 * autor:liman
 * createtime:2020/6/14
 * comment: 生产者和消费者最终版本
 */
@Slf4j
public class ProduceAndConsumerVersion03 {

    private int i = 0;
    private final Object LOCK = new Object();
    private volatile boolean isProduced = true;

    public void produce() {
        synchronized (LOCK) {
            while (isProduced) {//这里应该用循环去判断。如果这里用if,则后面需要补上else语句块
                try {
                    LOCK.wait();
                } catch (InterruptedException e) {
                    log.error("InterruptedException:{}", e);
                }
            }

            i++;
            log.info("p->,{}", i);
            LOCK.notifyAll();
            isProduced = true;
        }
    }

    public void consume() {
        synchronized (LOCK) {
            while (!isProduced) {//这里应该用循环去判断。如果这里用if,则后面需要补上else语句块
                try {
                    LOCK.wait();
                } catch (InterruptedException e) {
                    log.error("InterruptedException:{}", e);
                }
            }
            log.info("C->,{}", i);
            LOCK.notifyAll();
            isProduced = false;
        }
    }

    public static void main(String[] args) {
        ProduceAndConsumerVersion03 pc = new ProduceAndConsumerVersion03();
        Stream.of("P1").forEach(n -> new Thread(n) {
                    @Override
                    public void run() {
                        while (true) {
                            pc.produce();
                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                log.error("InterruptedException:{}", e);
                            }
                        }
                    }
                }.start()
        );

        Stream.of("C1", "C2", "C3", "C4","C5").forEach(n -> new Thread(n) {
                    @Override
                    public void run() {
                        while (true) {
                            pc.consume();
                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                log.error("InterruptedException:{}", e);
                            }
                        }
                    }
                }.start()
        );
    }
}

wait和sleep的区别

wait 方法

wait和notify方法其实并不是Thread特有的方法,而是属于Object的方法,意味着程序中的任何一个对象都有wait方法,wait方法有三个重载的方法,具体如下

//前两个方法底层调用的第三个方法
public final void wait() throws InterruptedException;//底层调用的是wait(0)
public final void wait(long timeout, int nanos) throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;//达到指定的时间,自动唤醒

1、当前线程执行了该对象的wait方法之后,会进入到该对象关联的wait set中,这也意味着一旦线程执行了某个object的wait方法之后,就会释放该对象的所有权(就是会释放锁),其他线程就有机会争抢该锁。

2、从实质来看,因为wait与对象相关,因此执行这个方法必须要拥有指定对象的monitor(锁标记),故而必须在同步方法或者同步代码块中使用该方法

3、notify是从对象的wait set中随机唤醒一个处于阻塞状态的线程,notifyAll则是唤醒wait set中的所有线程。

sleep方法

sleep方法是Thread的静态方法,只是让当前正在执行的线程,进入阻塞状态,但是当前线程不会释放锁。

两者区别

都是让线程进入阻塞状态,但是区别是很大的,也是面试中经常问的

不同点:

1、wait是属于Object对象的方法,而sleep是属于Thread的静态方法

2、wait必须在同步代码块或者同步方法中调用,而sleep没有这个限制

3、调用sleep的线程会在短暂休眠之后会主动退出阻塞,而调用wait方法的线程需要被其他线程唤醒才能退出阻塞

4、调用sleep的线程进入休眠的时候并不会释放对应的锁,而调用wait的方法会释放对应的锁

相同点:

1、都是可中断方法(在之前的博客中介绍过:可中断方法)

2、都可以使线程进入阻塞状态

一个综合实例

/**
 * autor:liman
 * createtime:2020/6/16
 * comment:模拟多个线程的运行,但是同时控制正在运行的线程数不超过5个
 */
@Slf4j
public class SelfThreadGroupService {
    private final static int MAX_WROKERSIZE = 5;
    //没有什么实质作用,只是做一个大小的限制判断
    final static private LinkedList<Control> CONTROLS = new LinkedList<>();

    public static void main(String[] args) {
        List<Thread> allWorkerThreadList = new ArrayList<>();
        //单纯的启动所有线程,并将线程翻入workerList
        Arrays.asList("M1","M2","M3","M4","M5","M6","M7","M8","M9","M10").stream()
                .map(SelfThreadGroupService::createWorkerThread)
                .forEach(t->{
                    t.start();
                    allWorkerThreadList.add(t);
                });

        //通过workerList进行join,因为不能再start的时候进行join操作,否则会无法做到并行
        allWorkerThreadList.stream().forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                log.error("线程执行异常,异常信息为:{}",e);
            }
        });
        log.info("所有的线程执行完毕");
    }

    private static Thread createWorkerThread(String name){
        return new Thread(()->{
            synchronized (CONTROLS) {
                //如果大于我们限制的数字,则等待
                while (CONTROLS.size() > MAX_WROKERSIZE) {
                    try {
                        CONTROLS.wait();
                    } catch (InterruptedException e) {
                        log.error("线程执行异常,异常信息为:{}",e);
                    }
                }
                //没有超限,则先进入到我们的缓存中
                CONTROLS.addLast(new Control());
            }

            //模拟当前线程的执行
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //执行完成之后,从缓存中移出
            synchronized (CONTROLS) {
                Optional.of("The worker [" + Thread.currentThread().getName() + "] END capture data.")
                        .ifPresent(System.out::println);
                CONTROLS.removeFirst();
                CONTROLS.notifyAll();
            }
        },name);
    }

    private static class Control {
    }

}

总结

本篇博客算是重新梳理了一下wait方法的作用与原理,希望能理解为什么wait方法必须要与一个对象关联,wait与sleep的区别,以及notify与notifyAll带来的一些问题,并且如何解决的,为什么wait建议一直在while判断中。这些也是有些公司面试常考的东西。

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