多线程 | 线程通信

线程之间的通信

前言

为什么要有线程通信?

​ 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,当然如果我们没有使用线程通信来使用多线程共同操作同一份数据的话,虽然可以实现,但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失!
​ 所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺。且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据

什么是线程通信?

​ 首先,线程间通信的方式有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析:

题目:有两个线程A、B,A线程向一个集合里面依次添加元素"abc"字符串,一共添加十次,当添加到第五次的时候,希望B线程能够收到A线程的通知,然后B线程执行相关的业务操作**

一种方式是使用 volatile 关键字

基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式

public class TestSync {
    // 定义一个共享变量来实现通信,它需要是volatile修饰,否则线程不能及时感知
    static volatile boolean notice = false;

    public static void main(String[] args) {
        List<String>  list = new ArrayList<>();
        // 实现线程A
        Thread threadA = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                list.add("abc");
                System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (list.size() == 5)
                    notice = true;
            }
        });
        // 实现线程B
        Thread threadB = new Thread(() -> {
            while (true) {
                if (notice) {
                    System.out.println("线程B收到通知,开始执行自己的业务...");
                    break;
                }
            }
        });
        // 需要先启动线程B
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 再启动线程A
        threadA.start();
    }
}

运行结果为

此方法虽然可以解决问题,但会造成资源争夺且易出错,不推荐。

下面引出线程通信常用的方式——等待唤醒机制

等待唤醒机制

​ 就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify)。Object类提供了线程间通信的方法:wait()notify()notifyaAl(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。

wait()

可以使当前执行的线程等待,暂停执行,直到接到通知或被中断为止

注意

  • 要确保调用wait()方法的时候拥有锁,即wait()方法只能在synchronized同步代码块中且只能由锁对象调用,否则会抛出IlegalMonitorStateExeption异常
  • 调用 wait()方法,当前线程会释放锁

wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁

notify()/notifyAll()

notify():notify()方法会唤醒一个等待当前锁的线程,若有多个等待的线程则会随机唤醒一个。

notifAll(): notifyAll()方法会唤醒等待当前锁的所有线程。

在同步代码块中调用 notify()方法后,并不会立即释放锁对象,需要等当前同步代码块执行完后才会释放锁对象,一般将 notify()方法放在同步代码块的最后

生产者消费者模型是我们学习多线程知识的一个经典案例,一个典型的生产者消费者模型如下:

    public void produce() {
        synchronized (this) {
            while (mBuf.isFull()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.add();
            notifyAll();
        }
    }

    public void consume() {
        synchronized (this) {
            while (mBuf.isEmpty()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.remove();
            notifyAll();
        }

    }

这段代码很容易引申出来两个问题:一个是wait()方法外面为什么是while循环而不是if判断,另一个是结尾处的为什么要用notifyAll()方法,用notify()行吗。

很多人在回答第二个问题的时候会想当然的说notify()是唤醒一个线程,notifyAll()是唤醒全部线程,但是唤醒然后呢,不管是notify()还是notifyAll(),最终拿到锁的只会有一个线程,那它们到底有什么区别呢?

其实这是一个对象内部锁的调度问题,要回答这两个问题,首先我们要明白java中对象锁的模型,JVM会为一个使用内部锁(synchronized)的对象维护两个集合,Entry SetWait Set,也有人翻译为锁池和等待池,意思基本一致。

对于Entry Set:如果线程A已经持有了对象锁,此时如果有其他线程也想获得该对象锁的话,它只能进入Entry Set,并且处于线程的BLOCKED状态。

对于Wait Set:如果线程A调用了wait()方法,那么线程A会释放该对象的锁,进入到Wait Set,并且处于线程的WAITING状态。

还有需要注意的是,某个线程B想要获得对象锁,一般情况下有两个先决条件,一是对象锁已经被释放了(如曾经持有锁的前任线程A执行完了synchronized代码块或者调用了wait()方法等等),二是线程B已处于RUNNABLE状态。

那么这两类集合中的线程都是在什么条件下可以转变为RUNNABLE呢?

对于Entry Set中的线程,当对象锁被释放的时候,JVM会唤醒处于Entry Set中的某一个线程,这个线程的状态就从BLOCKED转变为RUNNABLE。

对于Wait Set中的线程,当对象的notify()方法被调用时,JVM会唤醒处于Wait Set中的某一个线程,这个线程的状态就从WAITING转变为RUNNABLE;或者当notifyAll()方法被调用时,Wait Set中的全部线程会转变为RUNNABLE状态。所有Wait Set中被唤醒的线程会被转移到Entry Set中。

然后,每当对象的锁被释放后,那些所有处于RUNNABLE状态的线程会共同去竞争获取对象的锁,最终会有一个线程(具体哪一个取决于JVM实现,队列里的第一个?随机的一个?)真正获取到对象的锁,而其他竞争失败的线程继续在Entry Set中等待下一次机会。

有了这些知识点作为基础,上述的两个问题就能解释的清了。

首先来看第一个问题,我们在调用wait()方法的时候,心里想的肯定是因为当前方法不满足我们指定的条件,因此执行这个方法的线程需要等待直到其他线程改变了这个条件并且做出了通知。那么为什么要把wait()方法放在循环而不是if判断里呢,其实答案显而易见,因为wait()的线程不能确定其他线程会在什么状态下notify(),所以必须在被唤醒、抢占到锁并且从wait()方法退出的之前再次进行指定条件的判断,以决定是满足条件往下执行呢还是不满足条件再次wait()呢,归根结底是if只执行一次,而while是循环,可判断多次。

就像在本例中,如果只有一个生产者线程,一个消费者线程,那其实是可以用if代替while的,因为线程调度的行为是开发者可以预测的,生产者线程只有可能被消费者线程唤醒,反之亦然,因此被唤醒时条件始终满足,程序不会出错。但是这种情况只是多线程情况下极为简单的一种,更普遍的是多个线程生产,多个线程消费,那么就极有可能出现唤醒生产者的是另一个生产者或者唤醒消费者的是另一个消费者,这样的情况下用if就必然会现类似过度生产或者过度消费的情况了,典型如IndexOutOfBoundsException的异常。所以所有的java书籍都会建议开发者永远都要把wait()放到循环语句里面

然后来看第二个问题,既然notify()和notifyAll()最终的结果都是只有一个线程能拿到锁,那唤醒一个和唤醒多个有什么区别呢?

C1、C2(消费者线程)P1、P2 (生产者线程),假设初始时buffer是空的。

  1. C1、C2获得执行,发现buffer为空,因此调用wait阻塞,此时等待队列(WaitSet)里有两个线程C1、C2。

  2. P1拿到了锁,发现buffer为空,于是把它加满。

  3. P2 此时过来想要抢锁,发现锁被P1持有,于是放在同步队列里等待(_cxq/_EntryList),此时同步队列里有一个线程P2。

  4. P1加满buffer后调用notify将等待队列里的线程挪动到同步队列里,假设此处是C1被挪动到同步队列里。此时等待队列里有线程C2,同步队列里有线程P2、C1。

  5. 当P1退出临界区释放锁后,会唤醒同步队列里的线程,假设唤醒的是P2。

  6. P2获取锁后发现buffer是满的,于是调用wait释放锁并阻塞自己。此时同步队列里有线程C1,等待队列里有线程C2、P2。

  7. 同步队列里只有C1,C1获得锁后消费buffer,buffer变空,然后调用notify将等待队列里的线程挪到同步队列里,假设挪动的是C2。此时同步队列里有线C2,等待队列里有线程P2。

  8. C2获得锁后发现buffer为空,于是调用wait释放锁并挂起自己。此时同步队列为空,等待队列里有P2。

  9. 因为同步队列为空,所以C2并没有唤醒任何线程,而等待队列里的P2却是在苦苦等待。。再也没人唤醒它

但如果你把上述例子中的notify()换成notifyAll(),这样的情况就不会再出现了,因为每次notifyAll()都会使其他等待的线程从Wait Set进入Entry Set,从而有机会获得锁。

其实说了这么多,一句话解释就是之所以我们应该尽量使用notifyAll()的原因就是,notify()非常容易导致死锁。当然notifyAll并不一定都是优点,毕竟一次性将Wait Set中的线程都唤醒是一笔不菲的开销,如果你能handle你的线程调度,那么使用notify()也是有好处的。

还有notifyAll()是适用在公共的状态,比如这个例子是公共的状态,需要通过all 唤醒没问题,但是实际项目中是不会靠一个公共值维持线程的,一般是单独的对象对应一个同步锁,比如我是多个用户访问同个方法,每个用户都会独立wait(),如果是all 唤醒 那会唤醒全部,是不对的,必须要维持单独的对象锁,即每个对象唤醒本身,并设置wait时间!前面听all 多好多好,用了才发现,全醒是不行滴!请大家注意实际使用!

最后我把完整的测试代码放出来,供大家参考:

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

public class Something {
    private Buffer mBuf = new Buffer();

    public void produce() {
        synchronized (this) {
            while (mBuf.isFull()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.add();
            notifyAll();
        }
    }

    public void consume() {
        synchronized (this) {
            while (mBuf.isEmpty()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.remove();
            notifyAll();
        }
    }

    private class Buffer {
        private static final int MAX_CAPACITY = 1;
        private List innerList = new ArrayList<>(MAX_CAPACITY);

        void add() {
            if (isFull()) {
                throw new IndexOutOfBoundsException();
            } else {
                innerList.add(new Object());
            }
            System.out.println(Thread.currentThread().toString() + " add");

        }

        void remove() {
            if (isEmpty()) {
                throw new IndexOutOfBoundsException();
            } else {
                innerList.remove(MAX_CAPACITY - 1);
            }
            System.out.println(Thread.currentThread().toString() + " remove");
        }

        boolean isEmpty() {
            return innerList.isEmpty();
        }

        boolean isFull() {
            return innerList.size() == MAX_CAPACITY;
        }
    }

    public static void main(String[] args) {
        Something sth = new Something();
        Runnable runProduce = new Runnable() {
            int count = 4;

            @Override
            public void run() {
                while (count-- > 0) {
                    sth.produce();
                }
            }
        };
        Runnable runConsume = new Runnable() {
            int count = 4;

            @Override
            public void run() {
                while (count-- > 0) {
                    sth.consume();
                }
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runConsume).start();
        }
        for (int i = 0; i < 2; i++) {
            new Thread(runProduce).start();
        }
    }
}
  • 上面的栗子是正确的使用方式,输出的结果如下:
Thread[Thread-2,5,main] add
Thread[Thread-1,5,main] remove
Thread[Thread-3,5,main] add
Thread[Thread-0,5,main] remove
Thread[Thread-3,5,main] add
Thread[Thread-0,5,main] remove
Thread[Thread-2,5,main] add
Thread[Thread-1,5,main] remove

Process finished with exit code 0
  • 如果把while改成if,结果如下,程序可能产生运行时异常:
Thread[Thread-2,5,main] add
Thread[Thread-1,5,main] remove
Thread[Thread-3,5,main] add
Thread[Thread-1,5,main] remove
Thread[Thread-3,5,main] add
Thread[Thread-1,5,main] remove
Exception in thread "Thread-0" Exception in thread "Thread-2" java.lang.IndexOutOfBoundsException
    at Something$Buffer.add(Something.java:42)
    at Something.produce(Something.java:16)
    at Something$1.run(Something.java:76)
    at java.lang.Thread.run(Thread.java:748)
java.lang.IndexOutOfBoundsException
    at Something$Buffer.remove(Something.java:52)
    at Something.consume(Something.java:30)
    at Something$2.run(Something.java:86)
    at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0
  • 如果把notifyAll改为notify,结果如下,死锁,程序没有正常退出:
Thread[Thread-2,5,main] add
Thread[Thread-0,5,main] remove
Thread[Thread-3,5,main] add

常见问题

通知过早

线程wait()等待后,可以调用notify()唤醒线程,如果notify()唤醒的过早,在wait()等待之前就调用了notify()可能会打乱程序正常的运行逻辑。

所以若notify()通知过早,就不让线程等待了

public class WaitTest {
    static boolean isFirst = true;      //定义静态变量作为是否第一个运行的线程标志
    public static void main(String[] args) {
        final Object lock = new Object();   //定义锁对象
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    while (isFirst) {
                        try {
                            System.out.println("wait begin");
                            lock.wait();
                            System.out.println("wait end");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println("notify begin");
                    lock.notify();
                    System.out.println("notify end");
                    isFirst = false;
                }
            }
        });

//        t2.start();
//        t1.start();  //notify begin  notify end

        t1.start();
        t2.start();
        /*
          wait begin
          notify begin
          notify end
          wait end
         */
    }
}

实际上,调用start()就是告诉线程调度器,当前线程准备就绪,线程调度器在什么时候开启这个线程不确定,即调用start()方法的顺序,并不一定就是线程实际开启的顺序.

wait 等待条件发生了变化

public class WaitTest {
    //1.定义list集合
    static List list = new ArrayList<>();

    //2.定义方法从集合中取数据
     static public void subtract(){
        synchronized (list){
            while (list.size() == 0) {
                try {
                    System.out.println(Thread.currentThread().getName()+" wait begin");
                    list.wait();
                    System.out.println(Thread.currentThread().getName()+" wait end");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            list.remove(0);
            System.out.println("成功取出数据"+Thread.currentThread().getName()+" list.size="+list.size());
        }
    }

    //3)定义方法向集合中添加数据并唤醒等待的取数据的线程
    static public void add(){
        synchronized (list) {
            list.add("data");
            System.out.println(Thread.currentThread().getName()+"向集合中添加了数据");
            list.notifyAll();
        }
    }

    //4)定义线程类调用 add()取数据的方法
    static class ThreadAdd extends Thread{
        @Override
        public void run() {
            add();
        }
    }

    //定义线程类调用 subtract()方法
    static class ThreadSubtract extends Thread{
        @Override
        public void run() {
            subtract();
        }
    }

    public static void main(String[] args) {
        ThreadAdd threadAdd = new ThreadAdd();
        ThreadSubtract threadSubtract = new ThreadSubtract();
        threadSubtract.setName("subtract 1");

        //测试一: 先开启添加数据的线程,再开启一个取数据的线程,大多数情况下会正常取到数据
//        threadAdd.start();
//        threadSubtract.start();

        //测试二: 先开启取数据的线程,再开启添加数据的线程, 取数据的线程会先等待, 等到添加数据之后 ,再取数据
//        threadSubtract.start();
//        threadAdd.start();

        //测试三: 开启两个取数据的线程,再开启添加数据的线程
        ThreadSubtract threadSubtract2 = new ThreadSubtract();
        threadSubtract2.setName("subtract 2");
        threadSubtract.start();
        threadSubtract2.start();
        threadAdd.start();
    }
}

//测试1运行结果
Thread-0向集合中添加了数据
成功取出数据,subtract--list.size=0
//测试2运行结果
subtract 1 wait begin
Thread-0向集合中添加了数据
subtract 1 wait end
成功取出数据,subtract--list.size=0
//测试3运行结果
subtract 1 wait begin
subtract 2 wait begin
Thread-0向集合中添加了数据
subtract 2 wait end
成功取出数据subtract 2 list.size=0
subtract 1 wait end
subtract 1 wait begin

参考文档

https://blog.csdn.net/jisuanji12306/article/details/86363390

https://www.cnblogs.com/xiaowangbangzhu/p/10443103.html

https://www.jianshu.com/p/25e243850bd2?appinstall=0

posted @ 2021-05-31 17:11  至安  阅读(466)  评论(0编辑  收藏  举报