线程总结(六):线程通信

在实际编程中往往需要多个线程之间相互协作,这就需要线程之间的通信了,Java 提供了一些功能来完成线程之间的通信来保证线程的协调运行。

wait()、notify() 和 notifyAll()

Java 有一个机制来允许线程在等待信号的时候变成非运行状态,那就是 wait() 方法,这个方法并不是 Thread 类的,而是 Object 类提供的,相对应的还所有 notify() 和 notifyAll() 方法,下面来介绍一下这些方法:

wait() 方法

  • wait()

    在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
  • wait(long timeout)

    在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。
  • wait(long timeout, int nanos)

    在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。

notify() 方法

  • notify()

    唤醒在此对象监视器上等待的单个线程。

notifyAll() 方法

  • notifyAll()

    唤醒在此对象监视器上等待的所有线程。

如果一个线程调用了某个对象的 wait() 方法,该线程就会退出运行状态,直到另外一个线程调用了同一个对象的 notify() 或者 notifyAll() 方法。

需要注意的是,调用 wait() 或者 notify() 方法之前,线程必须先获取那个对象的同步锁,所以,wait()、notify()以及notifyAll() 方法需要运行在同步块或者同步方法内。

  • synchronized 方法的同步锁就是调用该方法的对象实例,所以可以在同步方法中直接调用这三个方法。
  • synchronized 修饰的代码块,同步锁就是 synchronized 小括号里面的那个对象,也就只有这个对象可以调用这三个方法。

简单示例
给定一个数字,然后开两个线程,一个线程给这个数字加一,另一个线程给这个数字减一,轮替执行。

public class ThreadCommTest {

    public static void main(String[] args) {

        NumberHolder holder = new NumberHolder(1000);

        Thread increaseThread = new NumberIncreaseThread(holder);
        increaseThread.setName("加线程");
        Thread reduceThread = new NumberReduceThread(holder);
        reduceThread.setName("减线程");
        increaseThread.start();
        reduceThread.start();

    }
}

class NumberIncreaseThread extends Thread {

    private NumberHolder numberHolder;

    public NumberIncreaseThread(NumberHolder numberHolder) {
        this.numberHolder = numberHolder;
    }

    @Override
    public void run() {
        while (true) {
            numberHolder.numberIncrease();
        }
    }
}

class NumberReduceThread extends Thread {

    private NumberHolder numberHolder;

    public NumberReduceThread(NumberHolder numberHolder) {
        this.numberHolder = numberHolder;
    }

    @Override
    public void run() {
        while (true) {
            numberHolder.numberReduce();
        }
    }
}

class NumberHolder {
    private int number;

    public NumberHolder(int number) {
        this.number = number;
    }

    public int getNumber() {
        System.out.println(Thread.currentThread().getName());
        return number;
    }

    public synchronized void numberIncrease() {
        try {
            notify();
            number += 1;
            System.out.println(Thread.currentThread().getName() + "线程 数字加一,此时 number = " + number);
            Thread.sleep(1000);
            wait();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public synchronized void numberReduce() {
        try {
            notify();
            number -= 1;
            System.out.println(Thread.currentThread().getName() + "线程 数字减一,此时 number = " + number);
            Thread.sleep(1000);
            wait();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

执行结果

减线程线程 数字减一,此时 number = 999
加线程线程 数字加一,此时 number = 1000
减线程线程 数字减一,此时 number = 999
加线程线程 数字加一,此时 number = 1000
减线程线程 数字减一,此时 number = 999
加线程线程 数字加一,此时 number = 1000
减线程线程 数字减一,此时 number = 999
加线程线程 数字加一,此时 number = 1000
减线程线程 数字减一,此时 number = 999
加线程线程 数字加一,此时 number = 1000
减线程线程 数字减一,此时 number = 999
加线程线程 数字加一,此时 number = 1000
减线程线程 数字减一,此时 number = 999
加线程线程 数字加一,此时 number = 1000

在这个例子中,减线程先获取到 CPU 的执行权,执行了 NumberHolder 的 numberReduce() 方法,因为这个方法是 synchronized 修饰的,那么减线程获取到同步监视器,其余线程无法访问 NumberHolder 对象,然后执行 notify() 方法(这个时候该方法没起啥作用),然后 number 减 1,输出,休眠 1 秒,然后执行 wait() 方法,减线程进入等待状态,让出 CPU 使用权,然后加线程获取到同步锁,调用 NumberHolder 的 numberIncrease() 方法,该方法同样也是 synchronized 修饰的,加线程同样也就获得同步锁,然后执行 notify() 方法,唤醒等待该对象监视器的线程(也就是减线程,但是现在同步锁在加线程手上,所以减线程得等待加线程释放锁),加 1,输出,休眠 1 秒,然后在进入等待状态释放同步锁,因为之前已经将减线程唤醒,所以减线程又开始执行,周而复始...

使用 wait、notify、notifyAll 方法的时候需要注意一下几点:

  • wait、notify、notifyAll 并不是线程 Thread 或者 Runnable 中的方法,而是 Object 类的方法。
  • 在调用 wait、notify、notifyAll 的时候,必须获得该对象的同步锁,必须在同步代码段中调用他们(synchronized 代码段或者 synchronized 方法)。
  • notify 方法会唤醒在此同步锁上等待的线程,假如同时有多个线程在等待,那么会随机唤醒其中一个。如果要唤醒全部,可以使用 notifyAll 方法。

Condition

我们使用 synchronized 来保证线程同步的时候,是有一个“隐形的”的“同步锁”的,线程们通过获取这个同步锁来保证同一时间只有一个线程可以访问共享资源。

但当我们使用 Lock 来保证线程同步的时候,就没有这个“隐形的”的“同步锁”了,也就没有办法再使用 wait、notify 和 notifuAll 来控制限制了。

Java 还提供了一个方式来满足我们对线程的控制需求:Condition,它是一个接口,通过调用 Lock 对象的 newCondition() 方法来获得,它同样提供了类似于 Object 类的 wait、notify 和 notifyAll 相同功能的方法,它拥有的方法如下:

  • await()

    造成当前线程在接到信号或被中断之前一直处于等待状态。
  • await(long time, TimeUnit unit)

    造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  • awaitNanos(long nanosTimeout)

    造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  • awaitUninterruptibly()

    造成当前线程在接到信号之前一直处于等待状态。
  • awaitUntil(Date deadline)

    造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
  • signal()

    唤醒一个等待线程。
  • signalAll()

    唤醒所有等待线程。

    简单示例

public class LockTest {

    public static void main(String[] args) {

        NumHolder num = new NumHolder(100);

        Thread thread1 = new NumberAddThread(num);
        Thread thread2 = new NumberDelThread(num);

        thread1.start();
        thread2.start();
    }
}

class NumberAddThread extends Thread {

    private NumHolder num;

    public NumberAddThread(NumHolder num) {
        this.num = num;
    }

    @Override
    public void run() {
        while (true) {

            try {
                Thread.sleep(1000);
                num.numberAdd();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

class NumberDelThread extends Thread {

    private NumHolder num;

    public NumberDelThread(NumHolder num) {
        this.num = num;
    }

    @Override
    public void run() {

        while (true) {
            try {
                Thread.sleep(1000);
                num.numberDel();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

class NumHolder {

    private int number;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public NumHolder(int number) {
        this.number = number;
    }

    public void numberAdd() {
        try {
            lock.lock();
            condition.signal();
            System.out.println(Thread.currentThread().getName() + " 加一");
            number += 1;
            System.out.println("此时 number = " + number);
            condition.await();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void numberDel() {
        try {
            lock.lock();
            condition.signal();
            System.out.println(Thread.currentThread().getName() + " 减一");
            number -= 1;
            System.out.println("此时 number = " + number);
            condition.await();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

Object 监视器和 Condition 的区别

对比项Object监视器 Condition
前置条件获取对象的锁调用Lock.lock获取锁,调用Lock.newCondition获取Condition对象
调用方式直接调用,比如object.notify()直接调用,比如condition.await()
等待队列的个数一个多个
当前线程释放锁进入等待状态支持支持
当前线程释放锁进入等待状态,在等待状态中不断响中断不支持支持
当前线程释放锁并进入超时等待状态支持支持
当前线程释放锁并进入等待状态直到将来的某个时间不支持支持
唤醒等待队列中的一个线程支持支持
唤醒等待队列中的全部线程支持支持