【Java多线程学习笔记三】线程间通信

wallhaven-mp9931.jpg


3. 线程间通信

技术点:

  • 使用 wait/notify 实现线程间的通信
  • 生产者/消费者模式的实现
  • 方法 join 的使用
  • ThreadLocal 类的使用
3.1 等待/通知机制
3.1.1 不使用等待/通知机制实现线程间通信

通过 while(true) 不断轮询,实现线程间通信。

缺点:如果轮询的时间间隔很小,更浪费 CPU 资源;如果轮询的时间间隔很大,有可能会取不到想要的数据,所以就需要有一种机制来实现减小 CPU 的资源浪费,而且还可以实现多个线程间通信,他就是【wait/notify】机制。

3.1.2 什么是等待/通知机制

等待/通知机制在生活的例子,比如在就餐时就会出现

  1. 厨师做完一道菜的时间不确定,所以厨师将菜品放到【菜品传递台】上的时间也不确定

    QQ20200529192200.png

  2. 服务员取到菜的时间取决于厨师,所以服务员就有【等待 wait】的状态

  3. 服务员如何能取得菜呢?这又取决于厨师,厨师将菜放在菜品传递台上,其实就相当于一种通知【notify】,这时服务员才可以拿到菜并交给就餐者

  4. 这个过程就相当于【等待/通知】机制

3.1.3 等待/通知机制的实现

方法 wait()的作用是使当前执行代码的线程进行等待,wait()方法是 Obejct 类的方法,该方法用来将当前线程置入【预执行】队列中,并且在 wait() 所在的代码行处停止执行,直到能够接到通知或中断为止。在调用 wait() 之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步代码块中调用 wait() 方法。在执行 wait() 方法后,当前线程释放锁,在从 wait() 返回前,线程与其他线程竞争重新获得锁。如果调用 wait() 时没有适当的锁,则抛出 IllegalMonitorStateException,它是 RuntimeException 的一个子类,因此,不需要 try-catch 语句进行捕捉异常。

方法 notify() 也要在同步方法或者同步代码块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用 notify() 时没有持有适当的锁,也会抛出 IllegalMonitorStateException。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选其中一个呈 wait 状态的线程,对其发出通知 notify,并使它等待获取该对象的对象锁。需要说明的是,在执行 notify() 方法后,当前线程不会马上释放对象锁,要等到执行 notify() 方法的线程将程序执行完,也就是退出 synchronized 代码块后,当前线程才会释放锁,而呈 wait状态所在的线程才可以获取该对象锁。当第一个获得了该对象锁的 wait 线程运行完毕后,他会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即使该对象已经空闲,其他 wait 状态等待的线程由于没有得到该对象的通知,还会继续阻塞在 wait 状态,直到这个对象发出一个 notify 或 notifyAll。

用一句话来总结 wait 和 notify:wait 使线程停止运行,而 notify 使停止的线程继续运行。

关键字 synchronized 可以将任何一个 Object 对象作为同步对象来看待,而 Java 为每个 Object 都实现了 wait() 和 notify() 方法,它们必须用在被 synchronized 同步的 Object 的临界区内。同时释放被同步对象的锁。而 notify 操作可以唤醒一个因调用了 wait 操作而处于阻塞状态的线程,使其进入就绪状态。被重新唤醒的线程会试图重新获得临界区的控制权,也就是锁,并继续执行临界区内 wait 之后的代码。如果发出 notify 操作时没有处于阻塞状态中的线程,那么该命令会被忽略。

wait() 方法可以使调用该方法的线程释放共享资源的锁然后从运行状态退出,进入等待队列,直到再次被唤醒。

notify() 方法可以随机唤醒等待队列中等待同一共享资源的一个线程并使该线程退出等待队列,进入可运行状态,也就是 notify() 方法仅通知一个线程。

notifyAll() 方法可以使所有正在等待队列中等待同一共享资源的全部线程从等待状态退出,进入可运行状态。此时,优先级最高的那个线程最先执行,但也可能是随机执行,这要取决于 JVM 虚拟机的实现。

status.jpg

  1. 新建一个线程对象后,再调用它的 start( ) 方法,系统就会为此线程分配 CPU 资源,使其处于 Runnable(可运行)状态,这是一个准备运行的阶段。如果线程抢占到 CPU 资源,此线程就处于 Running(运行)状态。

  2. Runnable 状态和 Running 状态可相互切换,因为有可能线程运行一段时间后,有其他优先级高的线程抢占了 CPU 资源,这时线程就从 Running 状态变成 Runnable 状态。

    线程进入 Runnable 状态大体可分为如下五种情况:

    • 调用 sleep( ) 方法后经过的时间超过了指定的休眠时间。
    • 线程调用的阻塞 IO 已经返回,阻塞方法执行完毕。
    • 线程成功地获得了试图同步的监视器。
    • 线程正在等待某个通知,其它线程发出了通知。
    • 处于挂起状态的线程调用了 resume 恢复方法。
  3. Blocked 是阻塞的意思,例如遇到了一个 IO 操作,此时 CPU 处于空闲状态,可能会转而把 CPU 时间片分配给其它线程,这是也可称为【暂停】状态。Blocked 状态结束后,进入 Runnable 状态,等待系统重新分配资源。

    出现阻塞的情况大体可分为如下 5 种:

    • 线程调用 sleep 方法,主动放弃占用的处理器资源。
    • 线程调用了阻塞式 IO 方法,在该方法返回前,该线程被阻塞。
    • 线程试图获得一个同步监视器,但该同步监视器正被其它线程所拥有。
    • 线程等待某个通知。
    • 程序调用了 suspend 方法将该线程挂起。此方法容易导致死锁,尽量避免使用该方法。
  4. run( ) 方法运行结束后进入销毁阶段,整个线程执行完毕。

    **每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。**一个线程被唤醒后,才会进入就绪队列,等待 CPU 的调度;反之,一个线程被 wait 后,就会进入阻塞队列,等待下一次被唤醒。

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

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

如果将 wait() 方法改为 sleep() 方法,就成了同步的效果,因为 sleep() 不释放锁。

3.1.5 当 interrupt 方法遇到 wait 方法

当线程呈 wait() 状态时,调用线程对象的 interrupt() 方法会出现 InterruptsedException 异常。

小总结:

  • 执行完同步代码块就会释放对象中的锁。
  • 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。
  • 在执行同步代码块的过程中,执行了锁所属对象的 wait() 方法,这个线程会释放对象所,而此线程对象会进入线程等待池中,等待被唤醒。
3.1.6 只通知一个线程

调用方法 notify() 一次只随机通知一个线程进行唤醒。

当多次调用 notify() 方法时,会随机将等待 wait 状态的线程进行唤醒。

3.1.7 唤醒所有进程

notifyAll() 方法可唤醒全部线程。

3.1.8 方法 wait(long) 的使用

带一个参数的 wait(long) 方法的功能是等待某一段时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒。

3.1.9 通知过早

如果通知过早,则会打乱程序正常的运行逻辑。

如果通知在 wait 前面,则 wait 永远不会被通知。

3.1.10 等待 wait 的条件发生变化
3.1.11 生产者/消费者模式实现

1. 一个生产者与一个消费者:操作值

现象:setValue() 与 getValue() 交替执行

package multithread.testProducerAndConsumer;


class P{
    private String lock;

    public P(String lock){
        super();
        this.lock = lock;
    }

    public void setValue(){
        try {
            synchronized (lock){
                if(!ValueObject.value.equals("")){
                    lock.wait();
                }
                String value = System.currentTimeMillis() + "_"
                        + System.nanoTime();
                System.out.println("set的值是:" + value);
                ValueObject.value = value;
                lock.notify();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

class C{
    private String lock;
    public C(String lock){
        super();
        this.lock = lock;
    }

    public void getValue(){
        try {
            synchronized (lock){
                if(ValueObject.value.equals("")){
                    lock.wait();
                }
                System.out.println("get 的值是:" + ValueObject.value);
                ValueObject.value = "";
                lock.notify();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

class ValueObject{
    public static String value = "";
}

class ThreadP extends Thread{
    private P p;

    public ThreadP(P p){
        super();
        this.p = p;
    }

    @Override
    public void run(){
        while (true){
            p.setValue();
        }
    }
}

class ThreadC extends Thread{
    private C c;

    public ThreadC(C c){
        super();
        this.c = c;
    }

    @Override
    public void run(){
        while (true){
            c.getValue();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        String lock = new String("");
        P p = new P(lock);
        C c = new C(lock);
        ThreadP pThread = new ThreadP(p);
        ThreadC cThread = new ThreadC(c);
        pThread.start();
        cThread.start();
    }
}

2. 多生产与多消费:操作值 - 假死

【假死】的现象其实激素hi线程进入 WAITING 等待状态。如果全部线程都进入了 WAITING 状态,则程序就不再执行任何业务功能了,整个项目呈停止状态。

从打印的信息来看,呈假死状态的进程中所有的线程都是呈 WAITING 状态,为什么会出现这样的情况呢?在代码中已经用了 wait/notify 了啊?

在代码中确实已经通过 wait/notify 进行通信了,但不保证 notify 唤醒的是异类,也许是同类,比如【生产者】唤醒【生产者】,或【消费者】唤醒【消费者】这样的情况。如果按这样情况运行的比率积少成多,就会导致所有的线程都不能运行下去,大家都在等待,都呈 WAITING 状态,程序最后也就呈【假死】状态,不能继续运行下去了。

解决办法:将 notify() 改为 notifyAll() 即可,它的原理就是不光通知同类线程,也包括异类,这样就不至于出现假死的状态了,程序就会一直运行下去。

4. 一生产与一消费:操作栈

5. 一生产与多消费 - 操作栈:解决 wait 条件改变与假死

使用一个生产者向堆栈对象中放入数据,而多个消费者从堆栈中取出数据。

要注意不能使用 notify 进行通知,因为会导致【假死】状态,要使用notifyAll。

6. 多生产者与一消费者:操作栈

3.1.12 通过管道进行线程间通信:字节流

在 Java 语言中提供了各种各样的输入/输出流 Stream,其中管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输出管道中读数据。通过使用管道,实现不同线程间的通信,而无需借助临时文件之类的东西。

  • PipedInputStream 和 PipedOutputStream
  • PipedReader 和 PipedWriter
3.1.13 通过管道进行线程间通信:字符流

待写…

3.1.14 实战:等待/通知之交叉备份

交叉打印

class DBTools{
    volatile private boolean prevIsA = false;
    synchronized public void backupA(){
        try {
            while (prevIsA == true){
                wait();
            }
            for (int i=0; i<5; i++){
                System.out.println("-----");
            }
            prevIsA = true;
            notifyAll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    synchronized public void backupB(){
        try {
            while (prevIsA == false){
                wait();
            }
            for (int i=0; i<5; i++){
                System.out.println("+++++");
            }
            prevIsA = false;
            notifyAll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

class BackupA extends Thread{
    private DBTools dbTools;

    public BackupA(DBTools dbTools){
        super();
        this.dbTools = dbTools;
    }

    @Override
    public void run(){
        dbTools.backupA();
    }
}

class BackupB extends Thread{
    private DBTools dbTools;
    public BackupB(DBTools dbTools){
        super();
        this.dbTools = dbTools;
    }

    @Override
    public void run(){
        dbTools.backupB();
    }
}

public class Run {
    public static void main(String[] args) {
        DBTools dbTools = new DBTools();
        for (int i=0; i<20; i++){
            BackupB output = new BackupB(dbTools);
            output.start();
            BackupA input = new BackupA(dbTools);
            input.start();
        }
    }
}

3.2 方法 join 的使用
3.2.1 学习方法 join 前的铺垫
3.2.2 用 join() 方法来解决

方法 join 的作用是使所属的线程对象 x 正常执行 run() 方法中的任务,而使当前线程 z 进行无限期的阻塞,等待线程 x 销毁后再继续执行线程 z 后面的代码。(没被 join 的线程阻塞,等待被 join 的线程执行完毕销毁后执行)

方法 join 具有使线程排队运行的作用,有些类似同步的运行效果。

join 与 sychronized 的区别是:join 在内部使用 wait() 方法进行等待,而 sychronized 关键字使用的是【对象监视器】原理做为同步。

3.2.3 方法 join 与异常

在 join 过程中,如果当前线程对象被中断,则当前线程出现异常。

3.2.4 方法 join(long) 的使用

方法 join(long) 中的参数是设定等待的时间。

3.2.5 方法 join(long) 与 sleep(long) 的区别

方法 join(long) 的功能在内部是使用 wait(long) 方法来实现的,所以 join(long) 方法具有释放锁的特点。

当执行 wait(long) 方法后,当前线程的锁被释放,那么线程就可以调用此线程中的同步方法了。

3.2.6 方法 join() 后面的代码提前运行:出现意外
3. 3 类 ThreadLocal 的使用

变量值的共享可以使用 public static 变量的形式,所有的线程都使用同一个 public static 变量。如果想实现每一个线程都有自己的共享变量该怎么解决呢?可以用 ThreadLocal 解决。

3.3.1 方法 get() 与 null
package multithread.testThreadLocal;

public class Run {
    public static ThreadLocal t1 = new ThreadLocal();

    public static void main(String[] args) {
        if(t1.get() == null){
            System.out.println("从未放过值");
            t1.set("我的值");
        }
        System.out.println(t1.get());
        System.out.println(t1.get());
    }
    // 结果:
    // 从未放过值
    // 我的值
    // 我的值
}

类 ThreadLocal 解决的是变量在不同线程之间的隔离性,也就是不同线程拥有的值,不同线程中的值是可以放入 ThreadLocal 类中进行保存的。

3.3.2 验证线程变量的隔离性

每个线程都有自己的空间,每个线程的变量的值都不同。

3.3.3 解决 get() 返回 null 问题
class ThreadLocalExt extends ThreadLocal{
    @Override
    protected Object initialValue(){
        return "我是默认值,第一个 get 不再为 null";
    }
}

public class Run{
    public static ThreadLocalExt t1 = new ThreadLocalExt();
    public static void main(String[] args){
        if(t1.get() == null){
            System.out.println("从未放过值");
            t1.set("我的值");
        }
        System.out.println(t1.get());
        System.out.println(t1.get());
    }
}
// 运行结果如下:
// 我是默认值,第一个 get 不再为 null
// 我是默认值,第一个 get 不再为 null

3.3.4 再次验证线程变量的隔离性
3.4 类 Inheritable 与 ThreadLocal 的使用

使用类 InheritableThreadLocal 可以在子线程中取得父线程继承下来的值。

3.4.1 值继承

使用 InheritableThreadLocal 类可以让子线程从父线程中取得值。

3.4.2 值继承再修改
class InheritableThreadLocalExt extends InheritableThreadLocal{
    // 初始化值
    @Override
    protected Object initialValue(){
        return new Date().getTime();
    }
    
    // 修改继承的值
    @Override
    protected Object childValue(Object parentValue){
        return parentValue + " 我在子线程加的~!";
    }
}

但要注意,如果子线程在取得值的同时,主线程将 InheritableThreadLocal 中的值进行更改,那么子线程取到的值还是旧值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章