《Java 多线程编程核心技术》笔记——第3章 线程中通信(一)

声明:

本博客是本人在学习《Java 多线程编程核心技术》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。

本博客已标明出处,如有侵权请告知,马上删除。

线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体。线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高 CPU 利用率的同时还会使程序员对各线程任务在处理的过程中进行有效的把控与监督。

在本章中需要着重掌握的技术点如下:

  • 使用 wait/notify 实现线程间的通信
  • 生产者/消费者模式的实现
  • 方法 join 的使用
  • ThreadLocal 类的使用

3.1 等待/通知机制

前面两章介绍了在 Java 语言中多线程的使用,以及对方法及变量在同步情况下的处理方式,本节将介绍多个线程之间进行通信,通过本节的学习可以了解到,线程与线程之间不是独立的个体,它们彼此之间可以互相通信和协作。

3.1.1 不使用等待/通知机制实现线程间通信

下面实验中使用 sleep() 结合 while(true) 死循环法来实现多个线程间通信。

  1. 创建一个公共类

    public class MyList {
          
          
        private volatile List list = new ArrayList<>();
    
        public void add() {
          
          
            list.add("anyString");
        }
    
        public int size() {
          
          
            return list.size();
        }
    
    }
    
  2. 创建两个自定义的线程类

    public class ThreadA extends Thread {
          
          
        private MyList myList;
    
        public ThreadA(MyList myList) {
          
          
            this.myList = myList;
        }
    
        @Override
        public void run() {
          
          
            super.run();
            try {
          
          
                for (int i = 0; i < 10; i++) {
          
          
                    myList.add();
                    System.out.println("添加了" + (i + 1) + "个元素");
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
    
    public class ThreadB extends Thread {
          
          
        private MyList myList;
    
        public ThreadB(MyList myList) {
          
          
            this.myList = myList;
        }
    
        @Override
        public void run() {
          
          
            super.run();
            try {
          
          
                while (true) {
          
          
                    if (myList.size() == 5) {
          
          
                        System.out.println("==5了,线程B要退出了");
                        throw new InterruptedException();
                    }
                }
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
    
  3. 测试类

    public class Test {
          
          
        public static void main(String[] args) {
          
          
            MyList myList = new MyList();
            ThreadA a = new ThreadA(myList);
            a.setName("A");
            a.start();
            ThreadB b = new ThreadB(myList);
            b.setName("B");
            b.start();
        }
    }
    
    

    运行结果

    添加了1个元素
    添加了2个元素
    添加了3个元素
    添加了4个元素
    java.lang.InterruptedException
    	at twothreadtransdata.ThreadB.run(ThreadB.java:18)
    添加了5个元素
    ==5了,线程B要退出了
    添加了6个元素
    添加了7个元素
    添加了8个元素
    添加了9个元素
    添加了10个元素
    

虽然两个线程间实现了通信,但有一个弊端就是,线程 ThreadB.java 不停地通过 while 语句轮询机制来检测某一个条件,这样会浪费 CPU 资源。

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

3.1.2 什么是等待/通知机制

等待/通知机制在生活中比比皆是,比如就餐时就会出现,如图 3-2 所示。

厨师和服务员之间的交互要在 “菜品传递台” 上,在这期间会有如下几个问题:

  1. 厨师师做完一道菜的时间不确定,所以厨师将菜品放到 “菜品传递台” 上的时间也不确定。
  2. 服务员取到菜的时间取决于厨师,所以服务员就有 “等待”(wait) 的状态。
  3. 服务员如何能取到菜呢?这又得取决于厨师,厨师将菜放在 “菜品传递台” 上,其实就相当于一种通知(notify),这时候服务员才可以拿到菜并交给就餐者。
  4. 这个过程中出现了 “等待/通知” 机制。

在这里插入图片描述

需要说明的是,前面章节中多线程之间也可以实现通信,原因就是多个线程共同访问同一个变量,但是那种同步机制不是 “等待/通知”,两个线程完全是主动的读取一个共享变量,在花费读取时间的基础上,读到的值是不是想要的,并不能完全确定。所以需要一种 “等待/通知” 的机制来满足上面的要求。

3.1.3 等待/通知机制的实现

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

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

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

实验 1

测试类

public class Test1 {
    
    
    public static void main(String[] args) {
    
    
        try {
    
    
            String s = new String("");
            s.wait();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

运行结果

Exception in thread "main" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at test1.Test1.main(Test1.java:7)

分析:出现异常的原因是没有 “对象监视器”,也就是没有同步加锁。

实验 2

测试类

public class Test2 {
    
    
    public static void main(String[] args) {
    
    
        try {
    
    
            String lock = new String();
            System.out.println("syn 上面");
            synchronized (lock) {
    
    
                System.out.println("syn 第一行");
                lock.wait();
                System.out.println("wait 下的代码");
            }
            System.out.println("syn 下面");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

运行结果

syn 上面
syn 第一行

分析:方法 wait 下面的代码不执行了,但线程不能永远等待下去,那样程序就停止不前,不继续向下运行了。如何使呈等待 wait 状态的线程继续运行呢?答案就是使用 notify() 方法。

实验 3

创建两个自定义的线程类

public class MyThread1 extends Thread {
    
    
    private Object lock;

    public MyThread1(Object lock) {
    
    
        this.lock = lock;
    }

    @Override
    public void run() {
    
    
        try {
    
    
            synchronized (lock) {
    
    
                System.out.println("开始 wait time = " + System.currentTimeMillis());
                lock.wait();
                System.out.println("结束 wait time = " + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}
public class MyThread2 extends Thread {
    
    
    private Object lock;

    public MyThread2(Object lock) {
    
    
        this.lock = lock;
    }

    @Override
    public void run() {
    
    
        synchronized (lock) {
    
    
            System.out.println("开始 notify time = " + System.currentTimeMillis());
            lock.notify();
            System.out.println("结束 notify time = " + System.currentTimeMillis());
        }
    }
}

测试类

public class Test {
    
    
    public static void main(String[] args) {
    
    
        try {
    
    
            Object lock = new Object();
            MyThread1 t1 = new MyThread1(lock);
            t1.start();
            Thread.sleep(3000);
            MyThread2 t2 = new MyThread2(lock);
            t2.start();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

运行结果

开始 wait time = 1597983575740
开始 notify time = 1597983578740
结束 notify time = 1597983578741
结束 wait time = 1597983578741

分析:从控制台打印的结果来看, 3 秒后线程被 notify 通知唤醒。

实验 4

如何使用 wait() 与 notify() 来实现前面 size() 值等于 5 的实验呢?

创建一个公共类

public class MyList {
    
    
    private static List list = new ArrayList<>();

    public static void add() {
    
    
        list.add("anyString");
    }

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

}

创建两个自定义的线程类

public class ThreadA extends Thread {
    
    
    private Object lock;

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

    @Override
    public void run() {
    
    
        try {
    
    
            synchronized (lock) {
    
    
                if (MyList.size() != 5) {
    
    
                    System.out.println("wait begin "+ System.currentTimeMillis());
                    lock.wait();
                    System.out.println("wait end "+ System.currentTimeMillis());
                }
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

public class ThreadB extends Thread {
    
    
    private Object lock;

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

    @Override
    public void run() {
    
    
        try {
    
    
            synchronized (lock) {
    
    
                for (int i = 0; i < 10; i++) {
    
    
                    MyList.add();
                    if (MyList.size() == 5) {
    
    
                        lock.notify();
                        System.out.println("已发出通知!");
                    }
                    System.out.println("添加了" + (i + 1) + "个元素");
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

测试类

public class Test {
    
    
    public static void main(String[] args) {
    
    
        try {
    
    
            Object lock = new Object();
            ThreadA a = new ThreadA(lock);
            a.start();
            Thread.sleep(500);
            ThreadB b = new ThreadB(lock);
            b.start();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

运行结果

wait begin 1597985787280
添加了1个元素
添加了2个元素
添加了3个元素
添加了4个元素
已发出通知!
添加了5个元素
添加了6个元素
添加了7个元素
添加了8个元素
添加了9个元素
添加了10个元素
wait end 1597985797787

分析:日志信息 wait end 在最后输出,这也说明 notify() 方法执行后并不立即释放锁。这个知识点在后面进行补充介绍。

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

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

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

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

在前面的章节中已经介绍了与 Thread 有关的大部分 API,这些 API 可以改变线程对象的状态,如图 3-7 所示。

在这里插入图片描述

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

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

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

    • 调用 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() 方法,锁却不自动释放

下面通过一个示例来演示:

  1. 创建一个公共类

    public class MyService {
          
          
        public void testMethod(Object lock) {
          
          
            try {
          
          
                synchronized (lock) {
          
          
                    System.out.println("begin wait()");
                    lock.wait();
                    System.out.println("end wait()");
                }
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
  2. 创建两个自定义的线程类

    public class ThreadA extends Thread{
          
          
        private Object lock;
    
        public ThreadA(Object lock) {
          
          
            this.lock = lock;
        }
    
        @Override
        public void run() {
          
          
            MyService myService = new MyService();
            myService.testMethod(lock);
        }
    }
    
    
    public class ThreadB extends Thread{
          
          
        private Object lock;
    
        public ThreadB(Object lock) {
          
          
            this.lock = lock;
        }
    
        @Override
        public void run() {
          
          
            MyService myService = new MyService();
            myService.testMethod(lock);
        }
    }
    
    
  3. 测试类

    public class Test {
          
          
        public static void main(String[] args) {
          
          
            Object lock = new Object();
            ThreadA a = new ThreadA(lock);
            a.start();
            ThreadB b = new ThreadB(lock);
            b.start();
        }
    }
    
    

    运行结果

    begin wait()
    begin wait()
    

    分析:方法 wait() 自动释放锁

  4. 如果将 wait() 方法改成 sleep() 方法,就成了同步的效果

    begin wait()
    end wait()
    begin wait()
    end wait()
    

    分析:sleep() 方法不释放锁

还有一个结论要进行实验:方法 notify() 被执行后,不释放锁。

下面通过一个示例来演示:

  1. 创建一个公共类

    public class Service {
          
          
        public void testMethod(Object lock) {
          
          
            try {
          
          
                synchronized (lock) {
          
          
                    System.out.println("begin wait() ThreadName=" + Thread.currentThread().getName());
                    lock.wait();
                    System.out.println("end wait() ThreadName=" + Thread.currentThread().getName());
                }
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    
        public void synNotifyMethod(Object lock) {
          
          
            try {
          
          
                synchronized (lock) {
          
          
                    System.out.println("begin notify() ThreadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
                    lock.notify();
                    Thread.sleep(5000);
                    System.out.println("end notify() ThreadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
                }
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    
    }
    
  2. 创建三个自定义的线程类

    public class ThreadA extends Thread {
          
          
        private Object lock;
    
        public ThreadA(Object lock) {
          
          
            this.lock = lock;
        }
    
        @Override
        public void run() {
          
          
            Service myService = new Service();
            myService.testMethod(lock);
        }
    }
    
    public class NotifyThread extends Thread {
          
          
        private Object lock;
    
        public NotifyThread(Object lock) {
          
          
            this.lock = lock;
        }
    
        @Override
        public void run() {
          
          
            Service myService = new Service();
            myService.synNotifyMethod(lock);
        }
    }
    
    public class SynNotifyMethodThread extends Thread {
          
          
        private Object lock;
    
        public SynNotifyMethodThread(Object lock) {
          
          
            this.lock = lock;
        }
    
        @Override
        public void run() {
          
          
            Service myService = new Service();
            myService.synNotifyMethod(lock);
        }
    }
    
  3. 测试类

    public class Test {
          
          
        public static void main(String[] args) {
          
          
            Object lock = new Object();
            ThreadA a = new ThreadA(lock);
            a.start();
            NotifyThread b = new NotifyThread(lock);
            b.start();
            SynNotifyMethodThread c = new SynNotifyMethodThread(lock);
            c.start();
        }
    }
    

    运行结果

    begin wait() ThreadName=Thread-0
    begin notify() ThreadName=Thread-1 time=1598001649957
    end notify() ThreadName=Thread-1 time=1598001654962
    end wait() ThreadName=Thread-0
    begin notify() ThreadName=Thread-2 time=1598001654962
    end notify() ThreadName=Thread-2 time=1598001659963
    

    分析:此实验说明,必须执行完 notify() 方法所在的同步 synchronized 代码块后才释放锁。

3.1.5 当 interrupt 方法遇到 wait 方法

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

  1. 创建一个公共类

    public class Service {
          
          
        public void testMethod(Object lock) {
          
          
            try {
          
          
                synchronized (lock) {
          
          
                    System.out.println("begin wait()");
                    lock.wait();
                    System.out.println("end wait()");
                }
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
                System.out.println("出现异常了,因为 wait 状态的线程被 interrupt 了!");
            }
        }
    
    }
    
  2. 创建一个自定义的线程类

    public class ThreadA extends Thread {
          
          
        private Object lock;
    
        public ThreadA(Object lock) {
          
          
            this.lock = lock;
        }
    
        @Override
        public void run() {
          
          
            Service service = new Service();
            service.testMethod(lock);
        }
    }
    
  3. 测试类

    public class Test {
          
          
        public static void main(String[] args) {
          
          
            try {
          
          
                Object lock = new Object();
                ThreadA a = new ThreadA(lock);
                a.start();
                Thread.sleep(5000);
                a.interrupt();
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    

    运行结果

    begin wait()
    出现异常了,因为 wait 状态的线程被 interrupt 了!
    java.lang.InterruptedException
    	at java.lang.Object.wait(Native Method)
    	at java.lang.Object.wait(Object.java:502)
    	at waitinterruptexception.Service.testMethod(Service.java:8)
    	at waitinterruptexception.ThreadA.run(ThreadA.java:13)
    

    通过上面的几个实验可以总结如下 3 点:

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

3.1.6 只通知一个线程

调用 notify() 一次只随机通知一个线程被唤醒

  1. 创建一个公共类

    public class Service {
          
          
        public void testMethod(Object lock) {
          
          
            try {
          
          
                synchronized (lock) {
          
          
                    System.out.println("begin wait() ThreadName=" + Thread.currentThread().getName());
                    lock.wait();
                    System.out.println("end wait() ThreadName=" + Thread.currentThread().getName());
                }
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
  2. 创建三个自定义的线程类

    public class ThreadA extends Thread {
          
          
        private Object lock;
    
        public ThreadA(Object lock) {
          
          
            this.lock = lock;
        }
    
        @Override
        public void run() {
          
          
            Service myService = new Service();
            myService.testMethod(lock);
        }
    }
    
    public class ThreadB extends Thread {
          
          
        private Object lock;
    
        public ThreadB(Object lock) {
          
          
            this.lock = lock;
        }
    
        @Override
        public void run() {
          
          
            Service myService = new Service();
            myService.testMethod(lock);
        }
    }
    
    public class ThreadC extends Thread {
          
          
        private Object lock;
    
        public ThreadC(Object lock) {
          
          
            this.lock = lock;
        }
    
        @Override
        public void run() {
          
          
            Service myService = new Service();
            myService.testMethod(lock);
        }
    }
    
  3. 创建唤醒线程

    public class NotifyThread extends Thread {
          
          
        private Object lock;
    
        public NotifyThread(Object lock) {
          
          
            this.lock = lock;
        }
    
        @Override
        public void run() {
          
          
            synchronized (lock) {
          
          
                lock.notify();
            }
        }
    }
    
  4. 测试类

    public class Test {
          
          
        public static void main(String[] args) {
          
          
            try {
          
          
                Object lock = new Object();
                ThreadA a = new ThreadA(lock);
                a.start();
                ThreadB b = new ThreadB(lock);
                b.start();
                ThreadC c = new ThreadC(lock);
                c.start();
                Thread.sleep(3000);
                NotifyThread notifyThread = new NotifyThread(lock);
                notifyThread.start();
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    

    运行结果

    begin wait() ThreadName=Thread-2
    begin wait() ThreadName=Thread-0
    begin wait() ThreadName=Thread-1
    end wait() ThreadName=Thread-2
    

    分析:方法notify()仅随机唤醒一个线程。

  5. 当多次调用 notify() 方法时,会随机将等待 wait 状态的线程进行唤醒。更改 NotifyThread.java 如下:

    public class NotifyThread extends Thread {
          
          
        private Object lock;
    
        public NotifyThread(Object lock) {
          
          
            this.lock = lock;
        }
    
        @Override
        public void run() {
          
          
            synchronized (lock) {
          
          
                lock.notify();
                lock.notify();
                lock.notify();
                lock.notify();
                lock.notify();
            }
        }
    }
    

    运行结果

    begin wait() ThreadName=Thread-0
    begin wait() ThreadName=Thread-1
    begin wait() ThreadName=Thread-2
    end wait() ThreadName=Thread-0
    end wait() ThreadName=Thread-2
    end wait() ThreadName=Thread-1
    

    分析:多次调用 notify() 方法唤醒了全部 WAITING 中的线程。

3.1.7 唤醒所有线程

前面示例中通过多次调用 notify() 方法来实现唤醒 3 个线程,但并不能保证系统中仅有 3 个线程,也就是若 notify() 方法的调用次数小于线程对象的数量,会出现有部分线程对象无法唤醒的情况。为了唤醒全部线程,可以使用 notifyAll() 方法

更改上面实验中的 NotifyThread.java 如下

public class NotifyThread extends Thread {
    
    
    private Object lock;

    public NotifyThread(Object lock) {
    
    
        this.lock = lock;
    }

    @Override
    public void run() {
    
    
        synchronized (lock) {
    
    
            lock.notifyAll();
        }
    }
}

运行结果

begin wait() ThreadName=Thread-0
begin wait() ThreadName=Thread-1
begin wait() ThreadName=Thread-2
end wait() ThreadName=Thread-2
end wait() ThreadName=Thread-1
end wait() ThreadName=Thread-0

分析:唤醒了所有线程

3.1.8 方法 wait(long) 的使用

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

下面通过一个示例来演示:

  1. 测试类

    public class MyRunnable {
          
          
        static private Object lock = new Object();
    
        static private Runnable runnable = new Runnable() {
          
          
            @Override
            public void run() {
          
          
                try {
          
          
                    synchronized (lock) {
          
          
                        System.out.println("wait begin time = " + System.currentTimeMillis());
                        lock.wait(5000);
                        System.out.println("end begin time = " + System.currentTimeMillis());
                    }
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
            }
        };
    
        public static void main(String[] args) {
          
          
            Thread t = new Thread(runnable);
            t.start();
        }
    }
    
    

    运行结果

    wait begin time = 1598011399759
    end begin time = 1598011404760
    

    分析:5 秒后自动被唤醒

  2. 当然也可以在 5 秒内由其他线程进行唤醒。代码更改如下:

    public class MyRunnable {
          
          
        static private Object lock = new Object();
    
        static private Runnable runnable = new Runnable() {
          
          
            @Override
            public void run() {
          
          
                try {
          
          
                    synchronized (lock) {
          
          
                        System.out.println("wait begin time = " + System.currentTimeMillis());
                        lock.wait(5000);
                        System.out.println("end begin time = " + System.currentTimeMillis());
                    }
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
            }
        };
    
        static private Runnable runnable2 = new Runnable() {
          
          
            @Override
            public void run() {
          
          
                synchronized (lock) {
          
          
                    System.out.println("notify begin time = " + System.currentTimeMillis());
                    lock.notify();
                    System.out.println("notify begin time = " + System.currentTimeMillis());
                }
            }
        };
    
        public static void main(String[] args) {
          
          
            try {
          
          
                Thread t = new Thread(runnable);
                t.start();
                Thread.sleep(3000);
                Thread t2 = new Thread(runnable2);
                t2.start();
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
    

    运行结果

    wait begin time = 1598012049400
    notify begin time = 1598012052400
    notify begin time = 1598012052400
    end begin time = 1598012052400
    

    分析:3 秒后由其他线程唤醒

3.1.9 通知过早

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

下面通过一个示例来演示:

  1. 测试类

    public class MyRun {
          
          
    
        private String lock = new String("");
    
        private Runnable runnableA = new Runnable() {
          
          
            @Override
            public void run() {
          
          
                try {
          
          
                    synchronized (lock) {
          
          
                        System.out.println("begin wait");
                        lock.wait();
                        System.out.println("end wait");
                    }
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
            }
        };
    
        private Runnable runnableB = new Runnable() {
          
          
            @Override
            public void run() {
          
          
                synchronized (lock) {
          
          
                    System.out.println("begin notify");
                    lock.notify();
                    System.out.println("end notify");
                }
            }
        };
    
        public static void main(String[] args) {
          
          
            MyRun myRun = new MyRun();
            Thread t1 = new Thread(myRun.runnableA);
            t1.start();
            Thread t2 = new Thread(myRun.runnableB);
            t2.start();
        }
    }
    
    

    运行结果

    begin wait
    begin notify
    end notify
    end wait
    

    分析:正常运行

  2. 如果将 main 方法中的代码改成如下

    public static void main(String[] args) {
          
          
        try {
          
          
            MyRun myRun = new MyRun();
            Thread t2 = new Thread(myRun.runnableB);
            t2.start();
            Thread.sleep(100);
            Thread t1 = new Thread(myRun.runnableA);
            t1.start();
        } catch (InterruptedException e) {
          
          
            e.printStackTrace();
        }
    
    }
    

    运行结果

    begin notify
    end notify
    begin wait
    

    分析:方法 wait 永远不会被通知

  3. 如果先通知了,则 wait 方法也必要执行了。在修改 MyRun 代码如下

    public class MyRun {
          
          
    
        private String lock = new String("");
        private boolean isFitstRunB = false;
    
        private Runnable runnableA = new Runnable() {
          
          
            @Override
            public void run() {
          
          
                try {
          
          
                    synchronized (lock) {
          
          
                        while (isFitstRunB == false) {
          
          
                            System.out.println("begin wait");
                            lock.wait();
                            System.out.println("end wait");
                        }
                    }
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
            }
        };
    
        private Runnable runnableB = new Runnable() {
          
          
            @Override
            public void run() {
          
          
                synchronized (lock) {
          
          
                    System.out.println("begin notify");
                    lock.notify();
                    System.out.println("end notify");
                    isFitstRunB = true;
                }
            }
        };
    
        public static void main(String[] args) {
          
          
            try {
          
          
                MyRun myRun = new MyRun();
                Thread t2 = new Thread(myRun.runnableB);
                t2.start();
                Thread.sleep(100);
                Thread t1 = new Thread(myRun.runnableA);
                t1.start();
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
    
        }
    }
    
    

    运行结果

    begin notify
    end notify
    

    分析:仅仅执行了 notify 方法

  4. 继续将上面程序中的 main 方法代码更改如下

    public static void main(String[] args) {
          
          
        try {
          
          
            MyRun myRun = new MyRun();
            Thread t1 = new Thread(myRun.runnableA);
            t1.start();
            Thread.sleep(100);
            Thread t2 = new Thread(myRun.runnableB);
            t2.start();
        } catch (InterruptedException e) {
          
          
            e.printStackTrace();
        }
    }
    

    运行结果

    begin wait
    begin notify
    end notify
    end wait
    

    分析:正常运行

3.1.10 等待 wait 的条件发生变化

在使用 wait/notify 模式时,还需要注意另外一种情况,也就是 wait 等待的条件发生了变化,也容易造成程序逻辑的混乱。

下面通过一个示例来演示:

  1. 创建三个公共类

    public class ValueObject {
          
          
        public static List<String> list = new ArrayList<>();
    }
    
    public class Add {
          
          
        private String lock;
    
        public Add(String lock) {
          
          
            this.lock = lock;
        }
    
        public void add() {
          
          
            synchronized (lock) {
          
          
                ValueObject.list.add("anyString");
                lock.notifyAll();
            }
        }
    }
    
    public class Subtract {
          
          
        private String lock;
    
        public Subtract(String lock) {
          
          
            this.lock = lock;
        }
    
        public void subtract() {
          
          
            try {
          
          
                synchronized (lock) {
          
          
                    if (ValueObject.list.size() == 0) {
          
          
                        System.out.println("wait begin ThreadName=" + Thread.currentThread().getName());
                        lock.wait();
                        System.out.println("wait end ThreadName=" + Thread.currentThread().getName());
                    }
                    ValueObject.list.remove(0);
                    System.out.println("list size=" + ValueObject.list.size());
                }
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
  2. 创建两个自定义的线程类

    public class ThreadAdd extends Thread{
          
          
        private Add add;
    
        public ThreadAdd(Add add){
          
          
            this.add = add;
        }
    
        @Override
        public void run() {
          
          
            add.add();
        }
    }
    
    public class ThreadSubtract extends Thread{
          
          
        private Subtract subtract;
    
        public ThreadSubtract(Subtract subtract){
          
          
            this.subtract = subtract;
    
        }
    
        @Override
        public void run() {
          
          
            subtract.subtract();
        }
    }
    
  3. 测试类

    public class Run {
          
          
        public static void main(String[] args) throws InterruptedException {
          
          
            String lock = new String("");
    
            Subtract s = new Subtract(lock);
            ThreadSubtract threadSubtract1 = new ThreadSubtract(s);
            threadSubtract1.setName("threadSubtract1");
            threadSubtract1.start();
            ThreadSubtract threadSubtract2 = new ThreadSubtract(s);
            threadSubtract2.setName("threadSubtract2");
            threadSubtract2.start();
    
            Thread.sleep(1000);
    
            Add add = new Add(lock);
            ThreadAdd threadAdd  = new ThreadAdd(add);
            threadAdd.setName("addThread");
            threadAdd.start();
        }
    }
    

    运行结果:

    wait begin ThreadName=threadSubtract1
    wait begin ThreadName=threadSubtract2
    wait end ThreadName=threadSubtract2
    list size=0
    wait end ThreadName=threadSubtract1
    Exception in thread "threadSubtract1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
    	at java.util.ArrayList.rangeCheck(ArrayList.java:653)
    	at java.util.ArrayList.remove(ArrayList.java:492)
    	at waitold.Subtract.subtract(Subtract.java:18)
    	at waitold.ThreadSubtract.run(ThreadSubtract.java:13)
    

    分析:出现异常的原因是因为有两个实现删除 remove() 操作的线程,他们在 Thread.sleep(1000); 之前都执行了 wait() 方法,呈等待状态,当加操作的线程在 1 秒后被运行时,通知了所有呈 wait 等待状态的减操作的线程,那么第一个实现减操作的线程能正确删除 list 中索引为 0 的数据。但第二个线程实现减操作的线程会出现索引溢出的异常,因为 list 中仅仅添加了一个数据,也只能删除一个数据,所以没有第二个数据可供删除。

  4. 如何解决这样的问题呢?更改 Subtract.java 中的 subtract 方法,代码如下:

    public class Subtract {
          
          
        private String lock;
    
        public Subtract(String lock) {
          
          
            this.lock = lock;
        }
    
        public void subtract() {
          
          
            try {
          
          
                synchronized (lock) {
          
          
                    while (ValueObject.list.size() == 0) {
          
          
                        System.out.println("wait begin ThreadName=" + Thread.currentThread().getName());
                        lock.wait();
                        System.out.println("wait end ThreadName=" + Thread.currentThread().getName());
                    }
                    ValueObject.list.remove(0);
                    System.out.println("list size=" + ValueObject.list.size());
                }
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    

    运行结果

    wait begin ThreadName=threadSubtract1
    wait begin ThreadName=threadSubtract2
    wait end ThreadName=threadSubtract2
    list size=0
    wait end ThreadName=threadSubtract1
    wait begin ThreadName=threadSubtract1
    

    分析:将 if 换成 while 后,list 为空时,就会继续执行循环,而不会删除元素了。

3.1.11 生产者/消费者模式实现

等待/通知模式最经典的案例就是 “生产者/消费者” 模式。但此模式在使用上有几种 “变形”,还有—些小的注意事项,但原理都是基于 wait/notify 的。

3.1.11.1 一生产与一消费:操作值

  1. 创建生产者

    public class P {
          
          
        private String lock;
    
        public P(String lock) {
          
          
            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();
            }
        }
    }
    
  2. 创建消费者

    public class C {
          
          
        private String lock;
    
        public C(String lock) {
          
          
            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();
            }
        }
    }
    
  3. 创建存储值的对象

    public class ValueObject {
          
          
        public static String value = "";
    }
    
  4. 创建两个自定义的线程类

    public class ThreadP extends Thread {
          
          
        private P p;
    
        public ThreadP(P p) {
          
          
            this.p = p;
        }
    
        @Override
        public void run() {
          
          
            while (true) {
          
          
                p.setValue();
            }
        }
    }
    
    public class ThreadC extends Thread {
          
          
        private C c;
    
        public ThreadC(C c) {
          
          
            this.c = c;
        }
    
        @Override
        public void run() {
          
          
            while (true) {
          
          
                c.getValue();
            }
        }
    }
    
  5. 测试类

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

    运行结果

    set的值是 1598153478124_582806771523454
    get的值是1598153478124_582806771523454
    set的值是 1598153478125_582806771741231
    get的值是1598153478125_582806771741231
    set的值是 1598153478125_582806771782120
    get的值是1598153478125_582806771782120
    ...
    

    分析:本示例是 1 个生产者和 1 个消费者进行数据的交互,在控制台中打印的日志 get 和 set 是交替运行的。但如果在此实验的基础上,设计出多个生产者和多个消费者,那么在运行的过程中极有可能出现 “假死” 的情况,也就是所有的线程都呈 WAITING 等待状态。

3.1.11.2 多生产与多消费:操作值——假死

“假死” 的现象其实就是线程进入 WAITING 等待状态。如果全部线程进入 WAITING 状态,则程序就不再执行任何业务功能了,整个项目呈停止状态。这在生产者与消费者模式中经常遇到。

  1. 创建生产者

    public class P {
          
          
        private String lock;
    
        public P(String lock) {
          
          
            this.lock = lock;
        }
    
        public void setValue() {
          
          
            try {
          
          
                synchronized (lock) {
          
          
                    while (!ValueObject.value.equals("")) {
          
          
                        System.out.println("生产者 " + Thread.currentThread().getName() + " WAITING");
                        lock.wait();
                    }
                    System.out.println("生产者 " + Thread.currentThread().getName() + " RUNNABLE");
                    ValueObject.value = System.currentTimeMillis() + " - " + System.nanoTime();
                    lock.notify();
                }
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
  2. 创建消费者

    public class C {
          
          
        private String lock;
    
        public C(String lock) {
          
          
            this.lock = lock;
        }
    
        public void getValue() {
          
          
            try {
          
          
                synchronized (lock) {
          
          
                    while (ValueObject.value.equals("")) {
          
          
                        System.out.println("消费者 " + Thread.currentThread().getName() + " WAITING");
                        lock.wait();
                    }
                    System.out.println("消费者 " + Thread.currentThread().getName() + " RUNNABLE");
                    ValueObject.value = "";
                    lock.notify();
                }
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
  3. 创建存储值的对象

    public class ValueObject {
          
          
        public static String value = "";
    }
    
  4. 创建两个自定义的线程类

    public class ThreadP extends Thread {
          
          
        private P p;
    
        public ThreadP(P p) {
          
          
            this.p = p;
        }
    
        @Override
        public void run() {
          
          
            while (true) {
          
          
                p.setValue();
            }
        }
    }
    
    public class ThreadC extends Thread {
          
          
        private C c;
    
        public ThreadC(C c) {
          
          
            this.c = c;
        }
    
        @Override
        public void run() {
          
          
            while (true) {
          
          
                c.getValue();
            }
        }
    }
    
  5. 测试类

    public class Run {
          
          
        public static void main(String[] args) throws InterruptedException {
          
          
            String lock = new String("");
            P p = new P(lock);
            C c = new C(lock);
            ThreadP[] threadPs = new ThreadP[2];
            ThreadC[] threadCs = new ThreadC[2];
            for (int i = 0; i < 2; i++) {
          
          
                threadPs[i] = new ThreadP(p);
                threadPs[i].setName("生产者" + i);
                threadCs[i] = new ThreadC(c);
                threadCs[i].setName("消费者" + i);
                threadPs[i].start();
                threadCs[i].start();
            }
    
            Thread.sleep(5000);
    
            Thread[] threadArray = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
            Thread.currentThread().getThreadGroup().enumerate(threadArray);
            for (int i = 0; i < threadArray.length; i++) {
          
          
                System.out.println(threadArray[i].getName() + " " + threadArray[i].getState());
            }
        }
    }
    

    运行结果

    ...
    生产者 生产者0 RUNNABLE
    生产者 生产者0 WAITING
    生产者 生产者1 WAITING
    消费者 消费者0 RUNNABLE
    消费者 消费者0 WAITING
    生产者 生产者0 RUNNABLE
    生产者 生产者0 WAITING
    生产者 生产者1 WAITING
    消费者 消费者1 RUNNABLE
    消费者 消费者1 WAITING
    消费者 消费者0 WAITING
    main RUNNABLE
    Monitor Ctrl-Break RUNNABLE
    生产者0 WAITING
    消费者0 WAITING
    生产者1 WAITING
    消费者1 WAITING
    

    分析:从打印信息来看,呈假死状态的进程中所有线程都呈 WAITING 状态。为什么会出现这种情况,代码中已经使用了 notify/wait 了?

    在代码中确实已经使用了 notify/wait 进行通信了,但不保证唤醒的就是异类,也许是同类。例如 “生产者” 唤醒 “生产者”,或 “消费者” 唤醒 “消费者” 这样的情况。如果经常这样,就会导致线程不能继续执行下去,所有线程都成 WAITING 状态,程序最后也呈 “假死” 状态,不能继续运行下去。

3.1.11.3 多生产与多消费:操作值

假死出现的主要原因是有可能连续唤醒同类。怎么能解决这样的问题呢?

不光唤醒同类,将异类也一同唤醒就解决了。

解决 “假死” 的情况很简单,只需将 notify() 方法改为 notifyAll() 方法就行,它的原理就是不光通知同类线程,也包括异类。这样就不至于出现假死的状态了,程序会一直运行下去。

3.1.11.4 一生产与一消费:操作栈

本示例是使生产者向堆栈 List 对象中放入数据,使消费者从 List 堆栈中取出数据。List 最大容量是 1,实验环境只有一个生产者与一个消费者。

  1. 创建堆栈

    public class MyStack {
          
          
        private List list = new ArrayList();
    
        synchronized public void push() {
          
          
            try {
          
          
                if (list.size() == 1) {
          
          
                    this.wait();
                }
                list.add("anyString= " + Math.random());
                this.notify();
                System.out.println("push=" + list.size());
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    
        synchronized public String pop() {
          
          
            String returnValue = "";
            try {
          
          
                if (list.size() == 0) {
          
          
                    System.out.println("pop 操作中的:" + Thread.currentThread().getName() + " 线程呈 wait 状态");
                    this.wait();
                }
                returnValue = "" + list.get(0);
                list.remove(0);
                this.notify();
                System.out.println("pop=" + list.size());
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
            return returnValue;
        }
    }
    
  2. 创建生产者

    public class P {
          
          
        private MyStack myStack;
    
        public P(MyStack myStack){
          
          
            this.myStack = myStack;
        }
    
        public void pushService(){
          
          
            myStack.push();
        }
    }
    
  3. 创建消费者

    public class C {
          
          
        private MyStack myStack;
    
        public C(MyStack myStack) {
          
          
            this.myStack = myStack;
        }
    
        public void popService() {
          
          
            System.out.println("pop= " + myStack.pop());
        }
    }
    
  4. 创建两个自定义的线程类

    public class PThread extends Thread {
          
          
        private P p;
    
        public PThread(P p) {
          
          
            this.p = p;
        }
    
        @Override
        public void run() {
          
          
            while (true) {
          
          
                p.pushService();
            }
        }
    }
    
    public class CThread extends Thread {
          
          
        private C c;
    
        public CThread(C c) {
          
          
            this.c = c;
        }
    
        @Override
        public void run() {
          
          
            while (true) {
          
          
                c.popService();
            }
        }
    }
    
  5. 测试类

    public class Run {
          
          
        public static void main(String[] args) {
          
          
            MyStack myStack = new MyStack();
            P p = new P(myStack);
            C c = new C(myStack);
            PThread pThread = new PThread(p);
            CThread cThread = new CThread(c);
            pThread.start();
            cThread.start();
        }
    }
    

    运行结果

    pop 操作中的:Thread-1 线程呈 wait 状态
    push=1
    pop=0
    pop= anyString= 0.9347111749963037
    pop 操作中的:Thread-1 线程呈 wait 状态
    push=1
    pop=0
    pop= anyString= 0.06822845166630898
    pop 操作中的:Thread-1 线程呈 wait 状态
    push=1
    pop=0
    pop= anyString= 0.4716816138207496
    pop 操作中的:Thread-1 线程呈 wait 状态
    push=1
    pop=0
    ...
    

    分析:程序运行结果 size() 不会大于1。

    通过使用生产者/消费者模式,容器的 size() 的值不会大于 1,这也是本例想要实现的效果,值在 0 和 1 之间切换,也就是生成和消费这两个过程在交替执行。

3.1.11.5 一生成与多消费——操作栈:解决 wait 条件改变和假死

本示例是使用一个生产者向堆栈 List 对象中放人数据,而多个消费者从 List 堆栈中取出数据。List 最大容量还是 1。

  1. 创建 Run2.java

    public class Run2 {
          
          
        public static void main(String[] args) {
          
          
            MyStack myStack = new MyStack();
            P p = new P(myStack);
            C c1 = new C(myStack);
            C c2 = new C(myStack);
            C c3 = new C(myStack);
            C c4 = new C(myStack);
            C c5 = new C(myStack);
            PThread pThread = new PThread(p);
            CThread cThread1 = new CThread(c1);
            CThread cThread2 = new CThread(c2);
            CThread cThread3 = new CThread(c3);
            CThread cThread4 = new CThread(c4);
            CThread cThread5 = new CThread(c5);
            pThread.start();
            cThread1.start();
            cThread2.start();
            cThread3.start();
            cThread4.start();
            cThread5.start();
        }
    }
    

    运行结果

    push=1
    pop=0
    pop= anyString= 0.0826590335944587
    pop 操作中的:Thread-4 线程呈 wait 状态
    pop 操作中的:Thread-3 线程呈 wait 状态
    pop 操作中的:Thread-2 线程呈 wait 状态
    pop 操作中的:Thread-1 线程呈 wait 状态
    pop 操作中的:Thread-5 线程呈 wait 状态
    push=1
    pop=0
    pop= anyString= 0.6385923471675526
    pop 操作中的:Thread-4 线程呈 wait 状态
    Exception in thread "Thread-3" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
        at java.util.ArrayList.rangeCheck(ArrayList.java:653)
        at java.util.ArrayList.get(ArrayList.java:429)
        at stack1.MyStack.pop(MyStack.java:29)
        at stack1.C.popService(C.java:11)
        at stack1.CThread.run(CThread.java:13)
    
    

    分析:此问题的出现就是因为在 MyStack.java 类中使用了 if 语句作为条件判断。因为条件发生改变时并没有得到及时的响应,所以多个呈 wait 状态的线程被唤醒,继而执行 list.remove(0) 代码而出现异常。解决这个办法是,将 if 改成 while 语句即可。

  2. 将 MyStack 的 if 都改成 while

    public class MyStack {
          
          
        private List list = new ArrayList();
    
        synchronized public void push() {
          
          
            try {
          
          
                while (list.size() == 1) {
          
          
                    this.wait();
                }
                list.add("anyString= " + Math.random());
                this.notify();
                System.out.println("push=" + list.size());
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    
        synchronized public String pop() {
          
          
            String returnValue = "";
            try {
          
          
                while (list.size() == 0) {
          
          
                    System.out.println("pop 操作中的:" + Thread.currentThread().getName() + " 线程呈 wait 状态");
                    this.wait();
                }
                returnValue = "" + list.get(0);
                list.remove(0);
                this.notify();
                System.out.println("pop=" + list.size());
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
            return returnValue;
        }
    }
    

    运行结果

    push=1
    pop=0
    pop= anyString= 0.10100384781458482
    pop 操作中的:Thread-4 线程呈 wait 状态
    pop 操作中的:Thread-3 线程呈 wait 状态
    pop 操作中的:Thread-2 线程呈 wait 状态
    pop 操作中的:Thread-1 线程呈 wait 状态
    push=1
    pop=0
    pop= anyString= 0.08542089888935511
    pop 操作中的:Thread-3 线程呈 wait 状态
    pop 操作中的:Thread-5 线程呈 wait 状态
    pop 操作中的:Thread-4 线程呈 wait 状态
    

    分析:执行结果没有异常,但是却出现了死锁。解决办法还是使用 notifyAll() 方法。代码两处使用了 notify 都改为 notifyAll 即可。

3.1.11.6 多生产与一消费

创建 Run3.java

public class Run3 {
    
    
    public static void main(String[] args) {
    
    
        MyStack myStack = new MyStack();
        P p1 = new P(myStack);
        P p2 = new P(myStack);
        P p3 = new P(myStack);
        P p4 = new P(myStack);
        P p5 = new P(myStack);
        C c = new C(myStack);

        PThread pThread1 = new PThread(p1);
        PThread pThread2 = new PThread(p2);
        PThread pThread3 = new PThread(p3);
        PThread pThread4 = new PThread(p4);
        PThread pThread5 = new PThread(p5);
        pThread1.start();
        pThread2.start();
        pThread3.start();
        pThread4.start();
        pThread5.start();

        CThread cThread = new CThread(c);
        cThread.start();
    }
}

3.1.11.7 多生产与多消费:操作栈

创建 Run4.java

public class Run4 {
    
    
    public static void main(String[] args) {
    
    
        MyStack myStack = new MyStack();
        P p1 = new P(myStack);
        P p2 = new P(myStack);
        P p3 = new P(myStack);
        P p4 = new P(myStack);
        P p5 = new P(myStack);

        C c1 = new C(myStack);
        C c2 = new C(myStack);
        C c3 = new C(myStack);
        C c4 = new C(myStack);
        C c5 = new C(myStack);

        PThread pThread1 = new PThread(p1);
        PThread pThread2 = new PThread(p2);
        PThread pThread3 = new PThread(p3);
        PThread pThread4 = new PThread(p4);
        PThread pThread5 = new PThread(p5);
        pThread1.start();
        pThread2.start();
        pThread3.start();
        pThread4.start();
        pThread5.start();

        CThread cThread1 = new CThread(c1);
        CThread cThread2 = new CThread(c2);
        CThread cThread3 = new CThread(c3);
        CThread cThread4 = new CThread(c4);
        CThread cThread5 = new CThread(c5);
        cThread1.start();
        cThread2.start();
        cThread3.start();
        cThread4.start();
        cThread5.start();
    }
}

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

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

在 JDK 中提供了 4 个类来使线程间可以进行通信

  1. PipedInputStream 和 PipedOutputStream
  2. PipedReader 和 PipedWriter

下面通过一个示例来演示:

  1. 创建输入、输出类

    public class WriteData {
          
          
        public void writeMethod(PipedOutputStream outputStream) {
          
          
            try {
          
          
                System.out.println("write:");
                for (int i = 0; i < 300; i++) {
          
          
                    String outData = "" + (i + 1);
                    outputStream.write(outData.getBytes());
                    System.out.println("write:" + outData);
                }
                outputStream.close();
            } catch (Exception e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
    public class ReadData {
          
          
        public void readMethod(PipedInputStream inputStream) {
          
          
            try {
          
          
                System.out.println("read:");
                byte[] byteArray = new byte[20];
                int readLength = inputStream.read(byteArray);
                while (readLength != -1) {
          
          
                    String newData = new String(byteArray, 0, readLength);
                    System.out.println("read:" + newData);
                    readLength = inputStream.read(byteArray);
                }
                System.out.println();
                inputStream.close();
            } catch (Exception e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
  2. 创建两个自定义的线程类

    public class ThreadRead extends Thread {
          
          
        private ReadData readData;
        private PipedInputStream inputStream;
    
        public ThreadRead(ReadData readData, PipedInputStream inputStream) {
          
          
            this.readData = readData;
            this.inputStream = inputStream;
        }
    
        @Override
        public void run() {
          
          
            readData.readMethod(inputStream);
        }
    }
    
    public class ThreadWrite extends Thread {
          
          
        private WriteData writeData;
        private PipedOutputStream outputStream;
    
        public ThreadWrite(WriteData writeData, PipedOutputStream outputStream) {
          
          
            this.writeData = writeData;
            this.outputStream = outputStream;
        }
    
        @Override
        public void run() {
          
          
            writeData.writeMethod(outputStream);
        }
    }
    
  3. 测试类

    public class Run {
          
          
        public static void main(String[] args) throws IOException, InterruptedException {
          
          
            PipedInputStream inputStream = new PipedInputStream();
            PipedOutputStream outputStream = new PipedOutputStream();
            WriteData writeData = new WriteData();
            ReadData readData = new ReadData();
    
            // 使两个线程之间通信
            // inputStream.connect(outputStream);
            outputStream.connect(inputStream);
    
            ThreadRead threadRead = new ThreadRead(readData, inputStream);
            threadRead.start();
    
            Thread.sleep(2000);
    
            ThreadWrite threadWrite = new ThreadWrite(writeData, outputStream);
            threadWrite.start();
        }
    }
    

    运行结果

    read:
    write:
    write:1
    write:2
    write:3
    ...
    write:298
    write:299
    write:300
    read:13713813914014114214
    read:31441451461471481491
    ...
    read:32842852862872882892
    read:90291292293294295296
    read:297298299300
    

    分析:从程序打印的结果来看,两个线程通过管道流成功进行数据的传输。

    但在此实验中,首先是读取线程 new ThreadRead(inputStream) 启动,由于当时没有数据被写人,所以线程阻塞在 int readLength= in.read(byteAray); 代码中,直到有数据被写入,才继续向下运行。

3.1.13 通过管道进行线程间通信:字符流

当然,在管道中还可以传递字符流。

  1. 创建输入、输出类

    public class WriteData {
          
          
        public void writeMethod(PipedWriter writer) {
          
          
            try {
          
          
                System.out.println("write:");
                for (int i = 0; i < 300; i++) {
          
          
                    String outData = "" + (i + 1);
                    writer.write(outData);
                    System.out.println("write:" + outData);
                }
                writer.close();
            } catch (Exception e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
    public class ReadData {
          
          
        public void readMethod(PipedReader reader) {
          
          
            try {
          
          
                System.out.println("read:");
                char[] charArray = new char[20];
                int readLength = reader.read(charArray);
                while (readLength != -1) {
          
          
                    String newData = new String(charArray, 0, readLength);
                    System.out.println("read:" + newData);
                    readLength = reader.read(charArray);
                }
                System.out.println();
                reader.close();
            } catch (Exception e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
  2. 创建两个自定义的线程类

    public class ThreadRead extends Thread {
          
          
        private ReadData readData;
        private PipedReader reader;
    
        public ThreadRead(ReadData readData, PipedReader reader) {
          
          
            this.readData = readData;
            this.reader = reader;
        }
    
        @Override
        public void run() {
          
          
            readData.readMethod(reader);
        }
    }
    
    public class ThreadWrite extends Thread {
          
          
        private WriteData writeData;
        private PipedWriter writer;
    
        public ThreadWrite(WriteData writeData, PipedWriter writer) {
          
          
            this.writeData = writeData;
            this.writer = writer;
        }
    
        @Override
        public void run() {
          
          
            writeData.writeMethod(writer);
        }
    }
    
  3. 测试类

    public class Run {
          
          
        public static void main(String[] args) throws IOException, InterruptedException {
          
          
            PipedReader reader = new PipedReader();
            PipedWriter writer = new PipedWriter();
    
            WriteData writeData = new WriteData();
            ReadData readData = new ReadData();
    
            //使两个线程之间通信
            writer.connect(reader);
    
            ThreadRead threadRead = new ThreadRead(readData, reader);
            threadRead.start();
    
            Thread.sleep(2000);
    
            ThreadWrite threadWrite = new ThreadWrite(writeData, writer);
            threadWrite.start();
        }
    }
    

    运行结果

    read:
    write:
    write:1
    write:2
    write:3
    write:4
    ...
    write:298
    write:299
    write:300
    read:52535455565758596061
    read:62636465666768697071
    ...
    read:81282283284285286287
    read:28828929029129229329
    read:4295296297298299300
    

    分析:打印的结果和前一个示例基本一样,此实验是在两个线程中通过管道流进行字符数据的传输。

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

本例创建 20 个线程,其中 10 个线程是将数据备份到 A 数据库中,另外 10 个线程将数据备份到 B 数据库中,并且备份 A 数据库和 B 数据库是交叉进行的。

首先创建出 20 个线程,效果如图 3-41 所示。

在这里插入图片描述

通过一些手段将这 20 个线程的运行效果变成有序的,如图 3-42 所示。

在这里插入图片描述

  1. 创建公共类

    public class DBTools {
          
          
        volatile private boolean preVIsA = false;
    
        synchronized public void backupA() {
          
          
            try {
          
          
                while (preVIsA == true) {
          
          
                    this.wait();
                }
                for (int i = 0; i < 5; i++) {
          
          
                    System.out.println("★★★★★");
                }
                preVIsA = true;
                notifyAll();
            } catch (Exception e) {
          
          
                e.printStackTrace();
            }
        }
    
        synchronized public void backupB() {
          
          
            try {
          
          
                while (preVIsA == false) {
          
          
                    this.wait();
                }
                for (int i = 0; i < 5; i++) {
          
          
                    System.out.println("☆☆☆☆☆");
                }
                preVIsA = false;
                notifyAll();
            } catch (Exception e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
  2. 创建两个自定义的线程类

    public class BackupA extends Thread {
          
          
        private DBTools dbTools;
    
        public BackupA(DBTools dbTools) {
          
          
            this.dbTools = dbTools;
        }
    
        @Override
        public void run() {
          
          
            dbTools.backupA();
        }
    }
    
    public class BackupB extends Thread {
          
          
        private DBTools dbTools;
    
        public BackupB(DBTools dbTools) {
          
          
            this.dbTools = dbTools;
        }
    
        @Override
        public void run() {
          
          
            dbTools.backupB();
        }
    }
    
  3. 测试类

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

    运行结果

    ★★★★★
    ★★★★★
    ★★★★★
    ★★★★★
    ★★★★★
    ☆☆☆☆☆
    ☆☆☆☆☆
    ☆☆☆☆☆
    ☆☆☆☆☆
    ☆☆☆☆☆
    ★★★★★
    ★★★★★
    ★★★★★
    ★★★★★
    ★★★★★
    ☆☆☆☆☆
    ☆☆☆☆☆
    ☆☆☆☆☆
    ☆☆☆☆☆
    ☆☆☆☆☆
    ...
    

    分析:执行的结果是交替运行的。

    交替打印的原理是使用下面的代码作为标记:

    volatile private boolean preVIsA = false;
    

    实现 A 和 B 线程交替执行的效果。

猜你喜欢

转载自blog.csdn.net/bm1998/article/details/108185482