多线程交替执行的一万种写法(记一道面试题)

多线程是 Java 的经典,也是重难点。很多时候,可能你反复运行了你的代码,确认没有了问题。但是很可能线上运行的某一天,突然出错挂了。事后反复寻找原因,却是死活重现不了场景。所以我们有必要深入地学习,不放过每一个细节。

题目

让两个线程依次打印 1A2B3C4D5E6F7G

谁都会想到的写法

我特地把这个最常见的用法放在最前边,由浅入深。也花了很多的篇幅,去描述这个最常见简单的写法,可能潜在的各种各样的古怪的 很难重现 的问题。

很多其他写法我放在了后边,因为最重要的是 夯实基础。有了这些知识,就能很容易把握好每一种写法。

这篇文章更多的是对多线程程序的 写法可能出现的问题 去进行 逻辑分析,以及我们在编写多线程程序需要 注意的各个点、细节,对很多底层的原理不会花过多详尽的解释,不然文章篇幅会过于长。
如果不懂 synchronized、volatile 等等原理的小伙伴可以去看我之前的一篇文章,里面详细介绍了线程 可见性、原子性 等内容,并从 虚拟机底层实现 分析了原理。
99%的人答不对的并发题(从JVM底层理解线程安全,硬核万字长文)

wait notify

用 synchronized 的 wait 、notify 来实现线程间的通信保证数据的正确
(如果这你还不知道,就先去补补基础吧)

public class T01_Wait_Notify {
    // 用于打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // synchronized 的对象
    static Object o = new Object();
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (o) {
                for(char c : array1) {
                    System.out.print(c);
                    o.notify();
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1").start();
        new Thread(() -> {
            synchronized (o) {
                for (char c : array2) {
                    System.out.print(c);
                    o.notify();
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t2").start();
    }
}

这个代码段是有问题的,不知道细心的你有没有看出来。
(你可以回头再分析一下,看看哪里会出差错)

给你看一下演示结果:
demo
似乎乍一看,好像没有啥问题。
你仔细看左边,发现这个程序并没有停止,而是继续在运行着。(不管怎样,它都不会正常终止)
我多运行几次,你再看看,竟然出现了不同的结果:
demo
你会神奇地发现,打印的顺序反了过来(从先数字后字母,变成了先字母后数字)
所以问题有两个:

  • 程序不能正常终止
  • 输出顺序有时会出错

其实问题原因并不复杂,关键是写代码时要很细心。很容易一不留神就有问题产生。
因为很多时候,同一段程序,运行的结果可能会不同。(由于线程调度的不确定性,这是多线程很头疼的地方)

我们继续分析上面的问题。
首先是程序不停止:我们在循环结束后加一句 notify 即可

synchronized (o) {
    for(char c : array1) {
        // 省略上述代码
    }
    o.notify(); // 循环结束后用notify
}

实际上,我们看上面的代码,就会发现,两个线程,在最终执行完所有的任务之后,最后都会调用 wait 方法。这样,总有一个线程先执行结束,一个后执行结束。后结束的线程最后先调用 notify 唤醒先执行完的线程让其退出,自己却 wait 在了那里。由于第一个线程已经终止退出了,所以没有线程再去唤醒它,此时它就会永远地在那里等待。
在循环结束后都加一个 notify 方法,这样保证了,不管哪一个线程先执行完,最后总归会再调用一次 notify 方法,这样就可以保证线程安全退出。

接下来我们再看第二个问题。我们虽然顺序执行了:

new Thread().start();
new Thread().start();

调整执行顺序

但是,线程调度不是程序员能手动控制的,所以两个线程谁先谁后这是不能够保证的。
所以诞生出这么一种写法,我们继续研究:
一个线程先 wait,一个线程先执行。

public static void main(String[] args) {
    new Thread(() -> {
        synchronized (o) {
            for(char c : array2) {
                try {
                    o.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(c);
                o.notify();
            }
            o.notify();
        }
    }, "t2").start();
    new Thread(() -> {
        synchronized (o) {
            for (char c : array1) {
                System.out.print(c);
                o.notify();
                try {
                    o.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }, "t1").start();
}

一看执行结果,似乎很完美。(于是我接着执行几次)
demo
(为了测试出这个 bug,我点了几十次,结果一直完好。。。于是我一生气,写了 10000 个循环,总算是出现死锁了)
可见多线程的问题很容易隐藏其中,不被发现
demo
(不要在意打印顺序,因为线程不止两个了,我开了循环重复了很多次才有那么一次出现死锁)
(当然也可以用 sleep 来进行测试)

现在我们分析代码:
其实我们的初衷是好的,让一个线程先等待,一个线程先执行。但是由于线程调度的不确定性,很可能一个线程先去执行并且 notify 了,然后这个线程才开始 wait,导致死锁。
所以就可能在某一次的执行过程中,程序在开始的第一步,才打印了一个字符,就两个线程就双双陷入等待。

所以之前的不管哪一种写法,都需要保证一点,就是两个线程的开始执行,必须保证先后次序。
第一种写法虽然可能打印相反,但是至少不会死锁。
第二种写法只要执行成功就一定打印正确,但是可能出现死锁。

伪唤醒

要保证线程的执行顺序,有很多种写法。可以用循环 CAS,LockSupport,CountDownLunch 等等。
不过我更推荐用 while 循环加 状态判断 的方式
因为在 JDK 的官方的 wait() 方法的注释中明确表示线程可能被 “虚假唤醒“,JDK 也明确推荐使用while 来判断状态信息。

这时,如果一个线程刚执行完任务,需要 另一个线程执行,它调用了 notify 将其唤醒,然后 wait 释放锁,开始等待……
重点来了: 结果它才开始等,就不知被谁叫醒了(伪唤醒),此时它又开始拿锁执行,如果抢到了锁,相当于连续执行了两次!!
另一个线程就只好等到它再一次 wait 释放锁时才能执行它的任务。

这个我就不测试了,因为概率实在太低了。。。所以很可能你拿几万次循环去跑都发现不了问题,以为完全正确了。(然而它暗暗地藏着问题,等上线了某一天就让你系统挂掉,然后你还找不到问题,因为错误也重现不了)
(太可怕了!!)

最终如下修改:
由于有一个状态来记录,所以就算是其它线程意外抢到了锁,也会放弃执行机会,让出锁让其他线程执行。

public class T03_Wait_Notify {
    // 用于打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // synchronized 的对象
    static Object o = new Object();
    // 添加一个状态来记录应该由谁打印 !!!!!!!!!!!!!
    volatile static int status = 1;
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (o) {
                for(char c : array1) {
                    // 循环保证不管什么时候意外获得了锁,只要状态不对都给打回去
                    while (1 != status) {
                        try {
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(c);
                    status = 2; // 执行完了之后修改标志位
                    o.notify();
                }
            }
        }, "t1").start();
        new Thread(() -> {
            synchronized (o) {
                for (char c : array2) {
                    // 循环保证不管什么时候意外获得了锁,只要状态不对都给打回去
                    while (2 != status) {
                        try {
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(c);
                    status = 1; // 执行完了之后修改标志位
                    o.notify();
                }
            }
        }, "t2").start();
    }
}

至少我暂时还是没出错过,而且真的找不出错了(要是真的还能有问题,请在评论区告诉我)
demo

wait notify 探究

有一个问题,为什么 wait notify 是 Object 的方法,而不是线程的方法?(因为 wait 是线程去等待,notify 唤醒的也是线程。这是一个非常矛盾的点)
首先,wait notify 这两个方法设计出来本身就是用来作为线程间通信使用的,而不是为了阻塞和唤醒线程。
实际上,曾经的 JDK 允许用 suspend() 和 resume() 方法来阻塞和唤醒线程,不过后来就被废弃了。
demo
而且我们也知道,Thread 类的 stop() 方法也是被废弃的。

因为本质上直接对指定的线程去操作是线程不安全的 !!!

因为线程调度的不确定性,我们去对一个线程直接操作的时候,往往很难保证线程当时的执行状态,所以直接对指定线程去操作很容易破坏线程的执行状态,造成程序异常。
所以我们很常用的 sleep() 方法是 Thread 类的 static 方法,而不是直接去操作 Thread 对象,让其睡眠。

而且 suspend 和 resume 方法是一个游离的不受管理控制的方法,很容易造成 死锁,而 synchronized 同步之后,在 同步锁 的保护之下,wait notify 就不容易出现死锁的情况。
因为在 synchronized 的同步保护下,一个线程的 notify方法虽然会唤醒线程,但是线程要进入同步块内,必须等到另一个线程释放锁才行,所以等到那个线程 wait 会释放锁,它才能够继续往下执行。
而用 suspend 和 resume 的方法,在 t1 唤醒 t2 之后,然后让 t1 进入等待,可是由于线程调度的不确定性,在 t1 进入等待之前,可能 t2 已经执行完了它的逻辑并且唤醒 t1,然后进入等待。那么这时 t1 才进入等待,那么它们就会双双睡去,程序卡死。
demo
在虚拟机内部,线程的 wait 与 notify 实际上是由一个对象监视器(Object Monitor)来管理的。
点开 Hotspot 虚拟机源码,我们看到:
objectmonitor
我们在 Hotspot 源码里也可以看到,对象监视器中有一个 WaitSet,就是我们常说的等待池。线程通过这些方法来通信,都是由 synchronized 所指定的对象的对象监视器来管理。
一个线程调用 Object 的 wait 方法,则会被加入到该对象的对象监视器的等待池中,而另一个线程调用 Object 的 notify 方法,则会唤醒等待池中的一个线程。
所以从虚拟机底层实现来说,wait notify 也必须是 Object 的方法,而不能直接操作线程。

文章目录位置说明

至于为什么我把文章目录放在这里,也是出于我的 良苦用心(防止有些小伙伴跳跃浏览)

有些小伙伴可能看到了标题,就想要疯狂的复制各种写法,然后去根面试官装 X。然而事实上,我给出的写法虽然较多,但是只要你愿意去想,你可以继续用更多的工具类去实现,或者设计出更优秀的算法。越往后面的各种写法,都更多的只是扩展一下知识面,以及锻炼思维的灵活性。

实际上,其他花里胡哨的写法,追根溯源,原理都是一样的。所以我们应该把最重要的,对于这类并发编程的 原理、思维、错误、细节 全部理解透彻,我们在实际中做到游刃有余。

LockSupport

我把这个放在第二,因为它是同样很重要的一个知识点。
最主要的原因是它的 优秀,即使程序员不够小心,它也能自己避免掉一些多线程的危险情况。

park、unpark

可能见过了之前出现过的各种错误和问题,你们已经很害怕了,非常谨慎地先照着之前 wait、notify 的程序去写。
我先给大家看一个示例

public class T04_LockSupport {
    // 用于打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // main
    public static void main(String[] args) {
        Thread t1;
        Thread t2;
        t1 = new Thread(() -> {
            for(char c : array1) {
                System.out.print(c);
                LockSupport.unpark(t2); // 叫醒t2
                LockSupport.park();     // 阻塞自己
            }
        });
        t2 = new Thread(() -> {
            for(char c : array2) {
                LockSupport.park();     // 阻塞自己
                System.out.print(c);
                LockSupport.unpark(t1);// 叫醒t1
            }
        });
        t1.start();
        t2.start();
    }
}

你可以先看一下有没有问题(我总感觉一般我这么问你们就算看不出有问题也觉得有问题)
实际上,你不用考虑是不是顺序死锁等问题。。
因为编译就会报错。。
demo
所以,这些代码看似简单,却很容易体现出一个程序员的扎实功底和细节辨析能力。
创建 t1 的时候还没有 t2,它里面的方法却用到了 t2,所以会出错。

public static void main(String[] args) {
    Thread t1 = null;
    Thread t2 = null;
    // 省略后面代码
}

这样是否会出错呢?仔细想想
答案是:
demo
不要崩溃,每出错一次,你就会学习到一点知识,积少成多,你就变成大牛。
上面说 lambda 表达式不行,那就再换

t1 = new Thread(){
    @Override
    public void run() {
        for(char c : array1) {
            System.out.print(c);
            LockSupport.unpark(t2); // 叫醒t2
            LockSupport.park();     // 阻塞自己
        }
    }
};

这次不用 lambda 表达式了,用重写 run 方法
答案是。。。。
demo
没关系,这里肯定会出错的。
因为实际上,对于内部类引用的本地变量,必须是 final 的,这是 Java 语言所规范的,否则就会报错。
而 lambda 表达式本质上就是 匿名内部类,所以也必须遵循规范。

所以这个 Thread 不能写在方法的局部变量中,而是需要把它放到成员变量里去。
经修改:

public class T05_LockSupport {
    // 用于打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // 两个线程
    static Thread t1 = null;
    static Thread t2 = null;
    // main
    public static void main(String[] args) {
        t1 = new Thread(() -> {
            for(char c : array1) {
                System.out.print(c);
                LockSupport.unpark(t2); // 叫醒t2
                LockSupport.park();     // 阻塞自己
            }
        });
        t2 = new Thread(() -> {
            for(char c : array2) {
                LockSupport.park();     // 阻塞自己
                System.out.print(c);
                LockSupport.unpark(t1); // 叫醒t1
            }
        });
        t1.start();
        t2.start();
    }
}

这样是否会出现死锁?

编译不报错了,我们再来探讨死锁问题。
经过上面 wait notify 的摧残,相信你们一下就能看出来。
要是 t1 先 unpark t2,然后 t2 再 park 自己,那这样就会双双陷入等待,程序永远不会结束。

然而事实上,不可能!!!

你可能要懵逼了,为啥这样不会出现死锁?
所以我在开头说 LockSupport 是非常优秀的,它会自己避免掉一些死锁情况,防止一些经验不足的程序猿总是写出漫天 bug 的代码。

LockSupport 底层是使用了 Unsafe(你不了解也没事)。首先当一个线程被 unpark 的时候,如果它当时还没有 park 住,它会记住它已经被 unpark 过了,这样下次再被 park 时,它会就会自动解除 park。这样就防止了先被 unpark 而导致死锁情况。

伪唤醒

和 wait notify 一样,LockSupport 的 park() 方法仍然存在伪唤醒的情况,所以我们也同样需要一个 while 循环来判断。
(这里与 wait notify 类似,大家应该很容易明白)

public class T06_LockSupport {
    // 用于打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // 两个线程
    static Thread t1;
    static Thread t2;
    // 添加一个状态来记录应该由谁打印
    volatile static int status = 1;
    // main
    public static void main(String[] args) {
        t1 = new Thread(() -> {
            for(char c : array1) {
                while (1 != status)
                    LockSupport.park(); // 阻塞自己
                System.out.print(c);
                status = 2;
                LockSupport.unpark(t2); // 叫醒t2
            }
        });
        t2 = new Thread(() -> {
            for(char c : array2) {
                while (2 != status)
                    LockSupport.park(); // 阻塞自己
                System.out.print(c);
                status = 1;
                LockSupport.unpark(t1); // 叫醒t1
            }
        });
        t1.start();
        t2.start();
    }
}

Lock Condition

既然你会用 synchronized 的 wait notify,那我相信你也一定会用 Lock 接口。
Lock 接口可以说实现并扩展了 synchronized 关键字(synchronized 可以实现的 Lock 都可以实现,并且 synchronized 不能实现的,Lock 也可以实现)。

(而且之后我不会再写很多理论知识了,因为 最重要 的那些点就是我 前两个大主题 所讲的知识,后面的更多是扩展知识面)

类似于 wait notify

(基本用法我这里不会讲,不会的要自己去乖乖补课哦)
(不同之处我用 注释 标出来了,关注不同点即可,就不用整片代码看了)

public class T07_Lock_Condition {
    // 用于打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // Lock Condition
    static Lock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    // 状态标志
    volatile static int status = 1;
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            lock.lock(); // 区别是显示加锁解锁
            try {
                for(char c : array1) {
                    while (1 != status) { // 与之前synchronized的while类似
                        try {
                            condition.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(c); // 可以看出代码实际上没太大变化
                    status = 2;
                    condition.signal();
                }
            } finally {
                lock.unlock(); // 区别是显示加锁解锁
            }
        }, "t1").start();
        new Thread(() -> {
            lock.lock(); // 区别是显示加锁解锁
            try {
                for(char c : array2) {
                    while (2 != status) { // 与之前synchronized的while类似
                        try {
                            condition.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(c); // 可以看出代码实际上没太大变化
                    status = 1;
                    condition.signal();
                }
            } finally {
                lock.unlock(); // 区别是显示加锁解锁
            }
        }, "t2").start();
    }
}

双 Condition

上面的写法,与我们改进后的 wait notify 是没有区别的,而且也不会出错。
不过,为了展现出 Lock 与 synchronized 不同的高级用法,可以创建出两个 Condition。t1 在一个 condition 等待,t2 在另一个 Condition 等待,这样就可以分别指定唤醒哪一个 Condition 对应的线程。
不过此处由于只有两个线程,同一时刻永远只有一个线程在等待,所以一个 Condition 已经足够。不过,如果线程数量增多,我们就可以利用到多 Condition 的优势了。
(不同之处我用 注释 标出来了,关注不同点即可,就不用整片代码看了)

public class T08_Lock_Condition {
    // 用于打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // Lock Condition
    static Lock lock = new ReentrantLock();
    static Condition condition1 = lock.newCondition(); // 两个condition
    static Condition condition2 = lock.newCondition();
    // 状态标志
    volatile static int status = 1;
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            lock.lock();
            try {
                for(char c : array1) {
                    while (1 != status) {
                        try {
                            condition1.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(c);
                    status = 2;
                    condition2.signal(); // 唤醒condition2的t2线程
                }
            } finally {
                lock.unlock();
            }
        }, "t1").start();
        new Thread(() -> {
            lock.lock();
            try {
                for(char c : array2) {
                    while (2 != status) {
                        try {
                            condition2.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(c);
                    status = 1;
                    condition1.signal(); // 唤醒condition2的t1线程
                }
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }
}

自旋

观察之前的代码我们可以发现,在 t1 结束后需要 t2 执行的时候,都是阻塞住自己,等 t2 执行完了再唤醒。
我们都知道,阻塞与唤醒一个线程因为涉及到用户态与内核态的切换,需要做很多现场保留还原、大量的验证等等一系列底层的复杂操作,所以会影响到程序的性能。因此,对于一些加锁内部代码很简单的小程序,我们完全不需要用锁将其阻塞,可以用 自旋 来保证安全。

while + 判断

public class T09_CAS {
    // 用于打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // 状态标志
    volatile static int status = 1;
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            for(char c : array1) {
                while (1 != status) {
                    // 空循环 等到另一个线程执行完修改状态
                }
                System.out.print(c);
                status = 2;
            }
        }, "t1").start();
        new Thread(() -> {
            for(char c : array2) {
                while (2 != status) {
                    // 空循环 等到另一个线程执行完修改状态
                }
                System.out.print(c);
                status = 1;
            }
        }, "t2").start();
    }
}

yield

不过之前的代码仍有些小小的不足。就是在 while 循环的时候,会消耗 CPU,它必须一直等待线程调度到另一个线程执行结束,它才能结束循环。
所以我们可以在 while 循环时加一个 yield() 方法,让它主动让出 CPU 去线程调度。虽然线程调度有不确定性,可能它让出 CPU 之后,仍然又被派回来继续执行。但是这样做仍可以增加提前结束循环的机会,节省一部分 CPU 资源。

while (1 != status) {
    Thread.yield();
}

BlockingQueue

可以用 BLockingQueue 阻塞队列来实现,一个线程执行结束,往队列中 put,另一个线程取到就继续。
这样可以保证安全,就比如一个苹果,你给他,他在给你,同一时刻总只可能有一个人拿着苹果。
示例:

public class T11_BlockingQueue {
    // 用于打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // BlockingQueue q1用来t2塞给t1东西 q2用来t1塞给t2东西
    static BlockingQueue<Object> q1 = new ArrayBlockingQueue<>(1);
    static BlockingQueue<Object> q2 = new ArrayBlockingQueue<>(1);
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            for(char c : array1) {
                System.out.print(c);
                try {
                    q2.put(new Object());
                    q1.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        new Thread(() -> {
            for(char c : array2) {
                try {
                    q2.take();
                    System.out.print(c);
                    q1.put(new Object());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t2").start();
    }
}

TransferQueue

TransferQueue 继承了 BlockingQueue 并扩展了一些新方法。生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列里就完事,可以说是专门为消息传递而诞生的)。

public class T13_TransferQueue {
    // 用于打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // TransferQueue
    static TransferQueue<Object> q1 = new LinkedTransferQueue<>();
    static TransferQueue<Object> q2 = new LinkedTransferQueue<>();
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            for(char c : array1) {
                try {
                    System.out.print(c);
                    queue.transfer(new Object());
                    queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        new Thread(() -> {
            for(char c : array2) {
                try {
                    queue.take();
                    System.out.print(c);
                    queue.transfer(new Object());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t2").start();
    }
}

PipedStream

很多小伙伴可能没有听说过 PipedStream 这么一个东西。不过这没有关系,因为这本来就没啥大用。。。。
这里说到这个呢仅仅是因为扩充一下知识面,到时候如果你在面试,对于这同一道面试题,你能写出那么多种的写法,自然会被面试官高看。

PipedInputStream、pipedOutputStream 都是输入输出流,是同步阻塞的,所以性能上比较差劲。而且线程的通信完全不需要这玩意,用共享变量,并且保证操作的线程安全即可。(这个仅仅是扩充下知识面)

public class T12_PipedStream {
    // 用于打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // PipedStream
    static PipedInputStream in1 = new PipedInputStream();
    static PipedInputStream in2 = new PipedInputStream();
    static PipedOutputStream out1 = new PipedOutputStream();
    static PipedOutputStream out2 = new PipedOutputStream();
    // main
    public static void main(String[] args) throws IOException {
        in1.connect(out1);
        in2.connect(out2); // 需要把input和output连接起来
        new Thread(() -> {
            for(char c : array1) {
                try {
                    System.out.print(c);
                    out2.write(2); // 写给t2
                    out2.flush();
                    in1.read();    // t1读
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        new Thread(() -> {
            for (char c : array2) {
                try {
                    in2.read();    // t2读
                    System.out.print(c);
                    out1.write(1); // 写给t1
                    out1.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }, "t2").start();
    }
}

题外话

实际上这道面试题最开始只是一道填空题,在 synchronized 的 wait 和 notify 方法位置将这两个代码挖掉,让同学们去填空,所以其实是很简单的。
但是,实际上,如果我们继续去深入研究这个知识,你会发现其实有很多知识点都可以延伸到,我们从中可以学习到很多的知识。
当然,最主要的,是要把我最上边写的最基本的 synchronized 的 wait notify 学习掌握透彻,理解在多线程中会产生的各种问题。那么,其他的各种写法对于你来说也一定是游刃有余了。

发布了9 篇原创文章 · 获赞 123 · 访问量 4543

猜你喜欢

转载自blog.csdn.net/weixin_44051223/article/details/104856584