互联网技术05——多线程通信wait/notify以及countDownLatch

场景带入

多线程之间处理数据,但业务复杂的时候,需要各个线程间实现通信,例如线程A和B同时处理数据,线程B的后半部分(假设是第50行代码,记做B50),必须等待线程A执行某方法(假设是第80行代码,记做A80)对数据进行初加工后B再执行。A、B同时启动,当A尚未执行完A80时,B线程已经运行到了B50,这时就需要B线程在B50处等待A80执行完毕,A80执行完毕后再通知B线程继续执行。这就涉及到了线程之间的通信。

实现手段:

  1. 使用wait/notify
  2. 使用concurrent.util包下的countDownLatch

wait/notify 必须配合synchronized关键字使用,同时所有的对象都具有这两个方法,因为他是object类的方法

wait释放锁,notify不释放锁(wait中的线程即使接收到notify通知,也要等发起notify通知的线程执行完毕后再执行)

阿里巴巴一道面试题:

代码逻辑是,有一个volatitle(多线程间内容可见)修饰的静态属性list,线程a向list中添加10个元素,当添加到第5个的时候,通知线程b已经到达5个了,线程b抛出异常跳出循环

package com.company;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by BaiTianShi on 2018/8/15.
 */
public class WaitNotify extends Thread{


    private volatile static List<String> list = new ArrayList<>();

    public void add(){
        list.add("memeda");
    }

    public int getSize(){
        return list.size();
    }

    public static void main(String[] args) {
        final WaitNotify wn = new WaitNotify();

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<10;i++){
                    System.out.println("向list添加第"+(i+1)+"个元素");
                    wn.add();
                    if(i == 4){
                        try {
                            Thread.sleep(500);
                            System.out.println("线程1等待0.5秒,给线程2判断list大小的时间");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                System.out.println("线程1执行结束");

            }
        },"t1");

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                    while (true){
                        if(wn.getSize() == 5){
                            System.out.println("当前线程收到通知:"+Thread.currentThread().getName()+"list size=5线程停止..");
                            throw new RuntimeException();
                        }
                    }

            }
        },"t2");
        thread.start();
        thread2.start();
    }


}

运行结果:

向list添加第1个元素
向list添加第2个元素
向list添加第3个元素
向list添加第4个元素
向list添加第5个元素
当前线程收到通知:t2list size=5线程停止..
Exception in thread "t2" java.lang.RuntimeException
	at com.company.WaitNotify$2.run(WaitNotify.java:51)
	at java.lang.Thread.run(Thread.java:745)
线程1等待0.5秒,给线程2判断list大小的时间
向list添加第6个元素
向list添加第7个元素
向list添加第8个元素
向list添加第9个元素
向list添加第10个元素
线程1执行结束

可见,在添加第5个的时候,线程2做出了正常判断并执行了抛出异常的代码。但是这样的方式是非常不理智的,因为线程2在抛出异常之前,一直以轮询的方式在运行,这就造成了性能的损耗。

通过wait和notify解决

package com.company;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by BaiTianShi on 2018/8/15.
 */
public class WaitNotify extends Thread{


    private volatile static List<String> list = new ArrayList<>();

    public void add(){
        list.add("memeda");
    }

    public int getSize(){
        return list.size();
    }

    public static void main(String[] args) throws InterruptedException {
        final WaitNotify wn = new WaitNotify();
        Object lock = new Object();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    for(int i=0;i<10;i++){
                        System.out.println("向list添加第"+(i+1)+"个元素");
                        wn.add();
                        if(i == 4){
                            lock.notify();
                            try {
                                Thread.sleep(500);
                                System.out.println("线程1等待0.5秒,给线程2判断list大小的时间");
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }

                System.out.println("线程1执行结束");

            }
        },"t1");

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    if(list.size() != 5){
                        try {
                            lock.wait();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("当前线程"+Thread.currentThread().getName()+"收到通知,线程停止..");
                    throw new RuntimeException();
//                    if(list.size() == 5){
//                        System.out.println("当前线程"+Thread.currentThread().getName()+"收到通知,线程停止..");
//                        throw new RuntimeException();
//                    }else {
//                        try {
//                            lock.wait();
//                        } catch (InterruptedException e) {
//                            e.printStackTrace();
//                        }
//                    }
                }


            }
        },"t2");
        thread2.start();
        Thread.sleep(2000);
        thread.start();

    }


}

 

执行结果

向list添加第1个元素
向list添加第2个元素
向list添加第3个元素
向list添加第4个元素
向list添加第5个元素
Exception in thread "t2" java.lang.RuntimeException
	at com.company.WaitNotify$2.run(WaitNotify.java:61)
	at java.lang.Thread.run(Thread.java:745)
线程1等待0.5秒,给线程2判断list大小的时间
向list添加第6个元素
向list添加第7个元素
向list添加第8个元素
向list添加第9个元素
向list添加第10个元素
线程1执行结束
当前线程t2收到通知,线程停止..


当前线程t2收到通知,线程并没有马上打印“...收到通知...”。这是因为notify不释放锁,只是通知等待中的线程,加入到竞争队列当中。

为了验证这一问题,将两个线程启动顺序调换,发现线程2一直在等待,不能结束

 t2.start();
 t.start();
改为
 t.start();
 t2.start();

这样,由于先执行线程"t"的话,线程"t1"便先获得锁开始执行,当list2中元素的个数达到5时虽然线程"t"调用了lock.notify();但是由于notify并不释放锁,因此线程"t"继续向下执行,list继续添加元素直到元素的个数达到10,线程"t"结束,这时线程"t2"才获得锁开始执行,但由于list2.size这时已经是10了,再也不会是5了,因此线程"t2"判断list2的元素个数不等于5,于是线程"t2"进入wait状态,线程"t1"已经结束了,没有线程去唤醒线程"t2"了,因此线程"t2"便一直处于等待状态了。

仍有问题:

但是这样的线程之间的通信机制虽然解决了性能消耗问题,但仍然有弊端。因为notify不释放锁,所以线程2必须等线程1完全执行完,线程2才能结束等待继续执行。这失去了线程间通信的实时性。

为解决这个问题,我们使用的工具类是CountDownLatch,该类还有个好处就是不用我们写synchronized关键字修饰了,我们在线程"t2"调用等待方法(countDownLatch.await();),在线程"t1"调用唤醒方法(countDownLatch.countDown();)。而且我们也不必纠结于线程"t1"和"t2"谁先启动谁后启动的问题,谁先启动都可以了。

package com.company;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * Created by BaiTianShi on 2018/8/15.
 */
public class WaitNotify extends Thread{


    private volatile static List<String> list = new ArrayList<>();

    public void add(){
        list.add("memeda");
    }

    public int getSize(){
        return list.size();
    }

    public static void main(String[] args) throws InterruptedException {
        final WaitNotify wn = new WaitNotify();
        CountDownLatch co = new CountDownLatch(1);
//        Object lock = new Object();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                    for(int i=0;i<10;i++){
                        System.out.println("向list添加第"+(i+1)+"个元素");
                        wn.add();
                        if(i == 4){
                            try {
                                Thread.sleep(500);
                                co.countDown();
                                System.out.println("线程1等待0.5秒,给线程2判断list大小的时间");
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }

                System.out.println("线程1执行结束");

            }
        },"t1");

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                    if(list.size() != 5){
                        try {
                            co.await();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("当前线程"+Thread.currentThread().getName()+"收到通知,线程停止..");
                    throw new RuntimeException();
            }
        },"t2");
        thread2.start();
        Thread.sleep(2000);
        thread.start();

    }


}

执行结果

向list添加第1个元素
向list添加第2个元素
向list添加第3个元素
向list添加第4个元素
向list添加第5个元素
当前线程t2收到通知,线程停止..
线程1等待0.5秒,给线程2判断list大小的时间
Exception in thread "t2" java.lang.RuntimeException
	at com.company.WaitNotify$2.run(WaitNotify.java:60)向list添加第6个元素

	at java.lang.Thread.run(Thread.java:745)
向list添加第7个元素
向list添加第8个元素
向list添加第9个元素
向list添加第10个元素
线程1执行结束

由于我的计算机向控制台打印代码时的耗时(也在另一方面说明了countDownLach通信的速度之快),造成部分打印顺序有些错乱,但是我们仍能发现,在第5个元素添加后,线程2马上就开始执行了,实现了实时性。

同时需要说明,countDownLach实例化时需要制定唤醒次数,如 countDownLach cd = new countDownLach(2);就需要各个线程中共执行两次cd.countDown()方法才能达到唤醒线程的目的

猜你喜欢

转载自blog.csdn.net/qq_28240551/article/details/81677183