Java基础笔记 - 线程间通信的实现 wait()和notify()方法

线程间的通信:

Java提供了3个非常重要的方法来巧妙地解决线程间的通信问题。这3个方法分别是:wait()、notify()和notifyAll()。它们都是Object类的最终方法,因此每一个类都默认拥有它们。

虽然所有的类都默认拥有这3个方法,但是只有在synchronized关键字作用的范围内,并且是同一个同步问题中搭配使用这3个方法时才有实际的意义。

死锁(deadlock):

所谓死锁,是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。 由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。

Object类中的两个方法:

wait
public final void wait(long timeout)
throws InterruptedException
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。
当前线程必须拥有此对象监视器(对象锁),即调用的wait方法一定在synchronized代码块或方法里面。
此方法导致当前线程(称之为 T)将其自身放置在对象的等待集中,然后放弃此对象上的所有同步要求。出于线程调度目的,在发生以下四种情况之一前,线程 T 被禁用,且处于休眠状态:
• 其他某个线程调用此对象的 notify 方法,并且线程 T 碰巧被任选为被唤醒的线程。
• 其他某个线程调用此对象的 notifyAll 方法。
• 其他某个线程中断线程 T。
• 大约已经到达指定的实际时间。但是,如果 timeout 为零,则不考虑实际时间,在获得通知前该线程将一直等待。

然后,从对象的等待集中删除线程 T,并重新进行线程调度。然后,该线程以常规方式与其他线程竞争,以获得在该对象上同步的权利;一旦获得对该对象的控制权,该对象上的所有其同步声明都将被恢复到以前的状态,这就是调用 wait 方法时的情况。然后,线程 T 从 wait 方法的调用中返回。所以,从 wait 方法返回时,该对象和线程 T 的同步状态与调用 wait 方法时的情况完全相同。

notify
public final void notify()
唤醒在此对象监视器(对象锁)上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。
直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
此方法只应由作为此对象监视器(对象锁)的所有者的线程来调用。通过以下三种方法之一,线程可以成为此对象监视器的所有者:
• 通过执行此对象的同步实例方法。
• 通过执行在此对象上进行同步的 synchronized 语句的正文。
• 对于 Class 类型的对象,可以通过执行该类的同步静态方法。

一次只能有一个线程拥有对象的监视器(对象锁)。

下面通过这两个方法实现线程间的通信:

首先创建一个Number2类,该类有一个add方法和一个sub synchronized方法:

class Number2 {

private int number;
//增加数量的synchronized方法
public synchronized void add(){
    //注意,这里不用if,因为如果有多个线程随意唤醒
    //下下一次随意唤醒线程时,可能就不会再作判断了,从而使判断失效
    while(0!=number){
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    number++;
    System.out.println(number);
    notify();
}
//减少数量的synchronized方法
public synchronized void sub(){
    while(0 == number){
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    number--;
    System.out.println(number);
    notify();
}

}

然后是调用add方法对应的线程类:

class AddThread extends Thread{
private Number2 number;

public AddThread(Number2 number){
    this.number = number;
}
@Override
public void run() {
    for(int i = 0; i<10; i++){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        number.add();
    }
}

}

接下来是调用sub方法对应的线程类:

class SubThread extends Thread{
private Number2 number;

public SubThread(Number2 number){
    this.number = number;
}
@Override
public void run() {
    for(int i = 0; i<10; i++){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        number.sub();
    }
}

}

在main方法中创建多个线程同时执行:

public static void main(String[] args) {
Number2 number = new Number2();
AddThread addThread = new AddThread(number);
SubThread subThread = new SubThread(number);
AddThread addThread2 = new AddThread(number);
SubThread subThread2 = new SubThread(number);
addThread.start();
subThread.start();
addThread2.start();
subThread2.start();
}

发现控制台只循环输出0和1。

关于wait和notify两个方法的说明:

两个方法都是定义在Object类中的final方法,因此所有的Java类都拥有此方法并且无法重写。

调用这两个方法之前要求线程获得对象的锁,即是对这两个方法的调用需要方法在synchronized方法或同步代码块中。

线程执行了wait方法时,线程进入等待,并释放掉对象锁。

wait方法与sleep方法的区别:

sleep方法只是让线程休眠了设置好的时间,但并不会释放获得的对象锁。而wait方法则会释放掉对象锁。

以下是加入线程间通信和对象锁后的线程状态图:

arthinking wechat
欢迎关注itzhai公众号