理解Java线程间的通信

多个线程间肯定是需要通信的,回忆一下我们的并发编程模型里的流水线工作模式(流水线工作模式),我们的线程在遇到io阻塞的时候,肯定不能在那边一直等着,得出让自己的cpu资源,等到io完成了,再把任务交给下一个工作线程,我们试着来实现下看看。


第一种方式,共享变量的方式。

package top.huster.thread.notify;

/**
 * Created by longan.rtb on 2018/10/31.
 */
public class WaitOnlineImpl {
    private static boolean finishedStatus = false;
    public static void main(String[] args) {
        Thread thread1  = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.print("我的工作做完了...");
                synchronized(this) {
                    finishedStatus = true;
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            while (!finishedStatus) {
                //not finish continue waiting
                //System.out.print("....");
            }
            System.out.print("我可以开始我的工作了...");
        });
        thread2.start();
        try {
            Thread.sleep(1000L);
        } catch (Exception e) {
            System.err.print("error");
        }
        thread1.start();
    }
}

//打印输入结果如下
我的工作做完了...我可以开始我的工作了...

问题,这种方式有个很明显的问题,线程2一直在while循环等待状态的改变,所以cpu在空转。
通知机制。幸好,java给我们提供了其他的方法。wait、notify·、notifyAll这三个方法能够让我们做到很多事情。后面的锁的实现很多也依赖于这个,所以一定要搞清楚他们。
使用wait、notify的方式实现如下

package top.huster.thread.notify;

/**
 * Created by longan.rtb on 2018/10/31.
 */
public class WaitNotify1Impl {
    private final static Object bell = new Object();
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 我的工作做完了...");
            synchronized (bell) {
                bell.notify();
            }
        });
        Thread thread2 = new Thread( () -> {
            synchronized (bell) {
                try {
                    System.out.println(Thread.currentThread().getName() + " I am waiting for somethings....");
                    bell.wait();
                } catch (Exception e) {
                    // do something
                }
            }
            System.out.println(Thread.currentThread().getName() + " 我开始我的工作.....");
        });
        thread2.start();
        try {
            Thread.sleep(1);
        } catch (Exception e) {
            //do something
        }
        thread1.start();
    }
}
/*** 
  输出结果
Thread-1 I am waiting for somethings....
Thread-0 我的工作做完了...
Thread-1 我开始我的工作.....
**/

成功的解决了空转的问题,但是我们仔细想一想会不会有问题呢?如果我们的程序先启动过了线程1,后启动了线程2,会有什么问题呢?
我们交换一下代码顺序,修改这几行代码如下

thread1.start();
try {
    Thread.sleep(1);
} catch (Exception e) {
    //do something
}
thread2.start();

运行结果如下,你会发现线程会一直等待而无法结束。

Thread-0 我的工作做完了...
Thread-1 I am waiting for somethings....

这个叫做信号丢失。也许简单的程序你不会犯错,但是程序一旦复杂,这种犯错的概率大大增加。那么如何来解决这个问题呢? 其实也很容易,我们设置一个标志位,如果已经notify了,则把这个标志位置为true,那么等待的线程如果发现标志位已经为true了,就不用wait了,修改代码如下:

package top.huster.thread.notify;

/**
 * Created by longan.rtb on 2018/10/31.
 */
public class WaitNotify1Impl {
    private final static Object bell = new Object();
    private static boolean hasNotified = false;
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 我的工作做完了...");
            synchronized (bell) {
                bell.notify();
                hasNotified = true;
            }
        });
        Thread thread2 = new Thread( () -> {
            synchronized (bell) {
                if (!hasNotified) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " I am waiting for somethings....");
                        bell.wait();
                    } catch (Exception e) {
                        // do something
                    }
                }
            }
            System.out.println(Thread.currentThread().getName() + " 我开始我的工作.....");
        });
        thread1.start();
        try {
            Thread.sleep(1);
        } catch (Exception e) {
            //do something
        }
        thread2.start();
    }
}
/***
输出结果如下 ,两个线程又愉快的生活在了一起。
Thread-0 我的工作做完了...
Thread-1 我开始我的工作.....
**/

是否现在就万事无忧了呢? 也不是,因为还有个问题叫,假唤醒。wait的线程在没有接收到notify的情况下,无缘无故的醒来,执行自己的逻辑。这个时候,因为依赖的notify线程并没有完成自己的工作,所以逻辑就出错了。这是我们不愿意看到的。
这个时候只需要把if的条件改成while就可以了,修改如下

Thread thread2 = new Thread( () -> {
    synchronized (bell) {
        while (!hasNotified) {
            try {
                System.out.println(Thread.currentThread().getName() + " I am waiting for somethings....");
                bell.wait();
            } catch (Exception e) {
                // do something
            }
        }
    }
    System.out.println(Thread.currentThread().getName() + " 我开始我的工作.....");
});

最开始我怎么也无法理解这个while替换if的妙处,因为我看到的文章并没有写任何逻辑,也就是线程没有做任何工作,我把print这个当做工作,写在while里,我想如果线程无端醒过来,不就继续执行他的工作了,而跟if和while没关系吗? 知道我把线程的实际工作从while拿出来才恍然大悟。只要标志位没有被设置为true,那么一直会在while循环里。都会执行wait逻辑,这个时候如果线程无端醒来,因为hasNotified还是false,因为又执行了一遍while逻辑,也就是再次进入wait。一直到标志位的值发生了改变,线程才有机会执行自己的逻辑。
实际的应用。我们使用wait和notify来实现一个锁试试。

package top.huster.thread.notify;

/**
 * Created by longan.rtb on 2018/11/1.
 */
public class LockTest {
    private int count = 0;
    private Lock lock = new Lock();
    public void increase() {
        lock.lock();
        count++;
        lock.unlock();
    }

    class Lock  {
        private boolean hasLocked = false;
        void lock() {
            synchronized(this)  {
                while (hasLocked) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                hasLocked = true;
            }
        }
        void unlock() {
            hasLocked = false;
            notify();
        }
    }
}

分析: 要特别注意while那一段代码,这个就是我们刚刚上面讲述的内容,如果没有这个while会导致这个lock错失notify的信号的话,就永远wait下去。而如果不用while用if的话,如果遇到假唤醒就会导致同时有两个线程进入了lock锁定区域,这个也与我们想要的不符合。

理解Java线程间的通信》有1个想法

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注