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

本文是《Java 多线程编程核心技术》第三章的笔记,重点探讨线程间通信的等待/通知机制。通过实例分析了wait(), notify(), notifyAll()方法的使用,以及中断、等待条件变化等情况对线程通信的影响,揭示了线程通信在提高CPU利用率和系统交互性中的重要性。" 109249758,9412117,链表排序:基于频度的排序算法,"['链表', '数据结构', '算法']

声明:

本博客是本人在学习《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 为空时,就会继续执行循环,而不会删除元素了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值