Java并发:一篇关于notify和Condition的使用感慨

165 阅读5分钟

一、synchronized和wait和notify的使用

这里为了让程序交替输出1和0,代码如下:

package com.test.notify;

public class TestNotify {
    private int count = 0;

    public synchronized void increase() {
        while (count != 0) { // 不能使用 if (count != 0)
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count++;
        System.out.print(count);
        notify();
    }

    public synchronized void decrease() {
        while (count == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count--;
        System.out.print(count);
        notify();
    }

    public static void main(String[] args) {
        TestNotify testNotify = new TestNotify();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    testNotify.increase();
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    testNotify.decrease();
                }
            }).start();
        }
    }
}

执行程序发现,有时候程序可以执行圆满结束,如:

有时候程序JVM会停止不下来,如:

很明显,存在线程处于阻塞的状态中而不能被唤醒

分析:为什么这段程序有时会存在不能被唤醒的线程?

为了思考起来比较简单,我们在程序里不设置10次循环了,这里假设仅设定循环2次,也就是创建了两个加1线程和两个减1线程。为了程序分析起来方便,我们分别给:

两个加线程命名为线程1、线程2;

两个减线程命名为线程3、线程4。

我们假设程序在运行时恰好是这样一种情况:

  • 1、线程3进入decrease方法,此时count值为0,判断count==0成立,进入while循环中并执行wait方法,此时线程1释放掉锁并被阻塞住
  • 2、线程4通过锁竞争也进入到了decrease方法,此时count值仍为0,判断count==0成立,进入while循环中并执行wait方法,此时线程4也释放掉锁并被阻塞住
  • 3、线程1进入increase方法,判断count!=0不成立,故而进行count++操作,count此时值变为了1,执行打印,并执行notify方法,线程1结束了(线程1彻底Game Over了,从此以后再也没线程1什么事了...)
  • 4、回到第三步骤中,线程1在死亡之前执行了notify方法,这个notify会唤醒线程3或者线程4中的一个
  • 5、这里假设线程1唤醒的是线程3,线程3准备从wait处继续执行,然后线程3和还没干什么事儿的线程2同时竞争锁,结果线程3竞争锁失败,只是进入了就绪状态,并没有被cpu调度执行
  • 6、线程2进入increase方法,此时count值为1,判断count!=0成立,进入while循环中并执行wait方法,此时线程2释放掉锁并被阻塞住
  • 7、线程2释放锁以后,线程4还处于被阻塞的状态,线程3顺理成章拿到锁接着执行,继续while循环判断条件,此时count值为1,判断count==0不成立,故而进行count--操作,count此时值变为了0,执行打印,并执行notify方法,线程3结束了(线程3也彻底Game Over了,从此以后再也没线程3什么事了...)
  • 8、此时只剩下线程2和线程4
  • 9、回到第7步骤中,线程3在死亡之前执行了notify方法,这个notify会唤醒线程2和线程4中的一个,线程2是加线程,线程4是减线程
  • 10、这里假设线程3唤醒的是线程4,也就是减线程,接着wait方法执行,继续判断while条件,此时count值为0,判断count==0成立,又进入到了while循环中并执行了wait方法,此时线程4又释放掉锁并被阻塞住!
  • 11、线程2不用管,还在阻塞着呢!
  • 12、程序从此结束不了了,线程2和线程4从此一直被阻塞了下去....

分析得出,程序之所以阻塞,是因为在唤醒的时候,唤醒了执行同类方法的线程(也就是减线程,恰好唤醒的是其他减线程),而没有唤醒需要唤醒的线程(加线程)。 所以我们引出了Condition类。

二、Condition的使用

Lock接口里有一个成员变量,是Condition类的实例。之所以引出这个Condition类,是因为Condition类提供了和wait、notify一样的功能。这里的区别是:notify在进行唤醒的时候,只有一个等待池(调用wait方法以后,线程会进入一个叫waitset的集合中,等待被notify唤醒);而与Lock实例所关联的condition实例,可以有多个,每个condition实例可以想象成类似waitset这样的集合,我们可以分别把不同的业务线程,通过await方法放入到不一样的condition当中,在唤醒时,调用不同condition实例的signal方法,来唤醒业务不同的线程。以下是代码:

package com.test.condition;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestCondition {
    public int count = 0;

    public Lock lock = new ReentrantLock();

    public Condition increaseCondition = lock.newCondition();

    public Condition decreaseCondition = lock.newCondition();

    public synchronized void increase() {
        lock.lock();
        try {
            while (count != 0) { // 不能使用 if (count != 0)
                try {
                    increaseCondition.await(); // 加1线程阻塞
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            count++;
            System.out.print(count);
            decreaseCondition.signal(); // 唤醒减1线程
        } finally {
            lock.unlock();
        }
    }

    public void decrease() {
        lock.lock();
        try {
            while (count == 0) { // 不能使用 if (count != 0)
                try {
                    decreaseCondition.await(); // 减1线程阻塞
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            count--;
            System.out.print(count);
            increaseCondition.signal(); // 唤醒加1线程
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        TestCondition testCondition = new TestCondition();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    testCondition.increase();
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    testCondition.decrease();
                }
            }).start();
        }
    }

}

执行结果:

减线程唤醒时,唤醒的是增线程; 增线程在唤醒时,唤醒的是减线程! 不会再发生notify唤醒同业务线程的尴尬情况了(不会再导致有的线程被阻塞住而不能被运行了)