理解Java线程间的通信

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


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

[code lang="java"]
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();
}
}

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

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

[code]
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 我开始我的工作.....
**/
[/code]

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

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

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

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

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

[code]
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 我开始我的工作.....
**/
[/code]

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

[code]
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() + " 我开始我的工作.....");
});
[/code]

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

[code]
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();
}
}
}
[/code]

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

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

发表回复

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