Java多线程交叉打印ABABAB,一个线程打印A,一个线程打印B

在Java中想要完成此功能有好几种方法都可以实现,这篇文章主要使用 wait 和 notifyAll 方法。

具体需求为:

要求先打印字符 A ,再打印字符 B ,完了再打印字符 A …如此循环下去,要求格式为:ABABABABABAB…

原理:

首先需要两个线程,一个打印字符 A ,另一个打印字符 B ,那么如何让他们互相协作呢?此时,我需要一个 boolean 类型的变量 flag ,这个变量可以理解为上次打印的字符是否是 A。如果变量为 true ,就表示上次打印的字符是 A ,反之则打印的是 B。

当打印字符 A 的线程即将打印时,我需要先判断这个变量 flag ,如果上次打印的是 A ,那么我就让这个线程进入 wait 状态,不能让它打印。如果让它打印了,那么结果就是 AA ,这种连续打印同一字符就是错误的。

同理,当打印字符 B 的线程即将打印时,我需要先判断这个变量 flag ,如果上次打印的是 B ,那么我就让这个线程进入 wait 状态。

紧接着,当判断 flag 不成立的时候,那就说明可以打印,就走下面的打印流程就是了,打印完了要将 flag 修改为对应的状态。之后再调用 notifyAll 方法环境在等待队列中等待的线程。

示例:

public class PrintABTest{

    // 该变量可以理解成:上一次打印是否是打印的字符 A。
    private volatile boolean flag = false;

    /**
     * 打印字符 A 的方法
     */
    private synchronized void printA(){
        try {
            // 判断上一次打印是否是打印的 A,如果是就进行等待,如果不是就执行下面的代码。
            while (flag){
                wait();
            }
            System.out.println("A");
            flag = true;
            // 唤醒在等待的线程
            notifyAll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    /**
     * 打印字符 B 的方法
     */
    private synchronized void printB(){
        try{
            // 判断上一次打印是否是打印的 B,如果是就进行等待,如果不是就执行下面的代码。
            // 注意这里是去反,因为上次打印如果不是A,肯定就是B。
            while (!flag){
                wait();
            }
            System.out.println("B");
            flag = false;
            // 唤醒在等待的线程
            notifyAll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    /**
     * main 方法
     * 启动线程调用打印方法
     */
    public static void main(String[] args) {
        // 创建实例
        PrintABTest printABTest = new PrintABTest();
        for (int i = 0; i < 300; i++) {
            // 打印 A
            new Thread(printABTest::printA).start();

            // 打印 B
            new Thread(printABTest::printB).start();
        }
    }
}

代码很简单,配合上述原理很好理解。下面思考两个问题:

  1. 原理一直在说判断 flag ,而判断为什么要用 while 而不用 if,用 if 会有什么效果?
  2. wait状态的线程被唤醒后,会重新执行临界区的代码还是接着上次执行的地方接着往下执行。

先解答第二个问题,其答案是:进入wait状态的线程被唤醒后,是接着上次执行的地方接着执行的。
先了解这个特性是为了弄清楚第一个问题的前提。具体验证过程可以参考:Java中进入wait状态的线程被唤醒后会接着上次执行的地方往下执行还是会重新执行临界区的代码

而第一个问题,为什么要用while,是因为当调用Java对象的notify()和notifyAll()方法时,会通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经满足过
为什么说是曾经满足过呢?因为notify()只能保证在通知时间点,条件是满足的。而被通知线程的执行时间点和通知的时间点基本上不会重合,所以当线程重新拿到执行权的时候,很可能条件已经不满足了(保不齐有其他线程插队)。换句话说就是当wait()返回时,有可能条件已经发生变化了,曾经条件满足,但是现在已经不满足了,所以要重新检验条件是否满足,这就是为什么要使用 while 去检查条件是否满足。这一点需要格外注意!!!。

尽量使用notifyAll()

在上面的代码中,我用的是notifyAll()来实现通知机制,为什么不使用notify()呢?这二者是有区别的,notify()是会随机地通知等待队列中的一个线程,而notifyAll()会通知等待队列中的所有线程。从感觉上来讲,应该是notify()更好一些,因为即便通知所有线程,也只有一个线程能够进入临界区。但那所谓的感觉往往都蕴藏着风险,实际上使用notify()也很有风险,它的风险在于可能导致某些线程永远不会被通知到。

假设我们有资源A、B、C、D,线程1申请到了AB,线程2申请到了CD,此时线程3申请AB,会进入等待队列(AB分配给线程1,线程3要求的条件不满足),线程4申请CD也会进入等待队列。我们再假设之后线程1归还了资源AB,如果使用notify()来通知等待队列中的线程,有可能被通知的是线程4,但线程4申请的是CD,所以此时线程4还是会继续等待,而真正该唤醒的线程3就再也没有机会被唤醒了。

所以除非经过深思熟虑,否则尽量使用notifyAll()。


技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章