场景带入
多线程之间处理数据,但业务复杂的时候,需要各个线程间实现通信,例如线程A和B同时处理数据,线程B的后半部分(假设是第50行代码,记做B50),必须等待线程A执行某方法(假设是第80行代码,记做A80)对数据进行初加工后B再执行。A、B同时启动,当A尚未执行完A80时,B线程已经运行到了B50,这时就需要B线程在B50处等待A80执行完毕,A80执行完毕后再通知B线程继续执行。这就涉及到了线程之间的通信。
实现手段:
- 使用wait/notify
- 使用concurrent.util包下的countDownLatch
wait/notify 必须配合synchronized关键字使用,同时所有的对象都具有这两个方法,因为他是object类的方法
wait释放锁,notify不释放锁(wait中的线程即使接收到notify通知,也要等发起notify通知的线程执行完毕后再执行)
阿里巴巴一道面试题:
代码逻辑是,有一个volatitle(多线程间内容可见)修饰的静态属性list,线程a向list中添加10个元素,当添加到第5个的时候,通知线程b已经到达5个了,线程b抛出异常跳出循环
package com.company;
import java.util.ArrayList;
import java.util.List;
/**
* Created by BaiTianShi on 2018/8/15.
*/
public class WaitNotify extends Thread{
private volatile static List<String> list = new ArrayList<>();
public void add(){
list.add("memeda");
}
public int getSize(){
return list.size();
}
public static void main(String[] args) {
final WaitNotify wn = new WaitNotify();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("向list添加第"+(i+1)+"个元素");
wn.add();
if(i == 4){
try {
Thread.sleep(500);
System.out.println("线程1等待0.5秒,给线程2判断list大小的时间");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("线程1执行结束");
}
},"t1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
if(wn.getSize() == 5){
System.out.println("当前线程收到通知:"+Thread.currentThread().getName()+"list size=5线程停止..");
throw new RuntimeException();
}
}
}
},"t2");
thread.start();
thread2.start();
}
}
运行结果:
向list添加第1个元素
向list添加第2个元素
向list添加第3个元素
向list添加第4个元素
向list添加第5个元素
当前线程收到通知:t2list size=5线程停止..
Exception in thread "t2" java.lang.RuntimeException
at com.company.WaitNotify$2.run(WaitNotify.java:51)
at java.lang.Thread.run(Thread.java:745)
线程1等待0.5秒,给线程2判断list大小的时间
向list添加第6个元素
向list添加第7个元素
向list添加第8个元素
向list添加第9个元素
向list添加第10个元素
线程1执行结束
可见,在添加第5个的时候,线程2做出了正常判断并执行了抛出异常的代码。但是这样的方式是非常不理智的,因为线程2在抛出异常之前,一直以轮询的方式在运行,这就造成了性能的损耗。
通过wait和notify解决
package com.company;
import java.util.ArrayList;
import java.util.List;
/**
* Created by BaiTianShi on 2018/8/15.
*/
public class WaitNotify extends Thread{
private volatile static List<String> list = new ArrayList<>();
public void add(){
list.add("memeda");
}
public int getSize(){
return list.size();
}
public static void main(String[] args) throws InterruptedException {
final WaitNotify wn = new WaitNotify();
Object lock = new Object();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock){
for(int i=0;i<10;i++){
System.out.println("向list添加第"+(i+1)+"个元素");
wn.add();
if(i == 4){
lock.notify();
try {
Thread.sleep(500);
System.out.println("线程1等待0.5秒,给线程2判断list大小的时间");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
System.out.println("线程1执行结束");
}
},"t1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
if(list.size() != 5){
try {
lock.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("当前线程"+Thread.currentThread().getName()+"收到通知,线程停止..");
throw new RuntimeException();
// if(list.size() == 5){
// System.out.println("当前线程"+Thread.currentThread().getName()+"收到通知,线程停止..");
// throw new RuntimeException();
// }else {
// try {
// lock.wait();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
}
}
},"t2");
thread2.start();
Thread.sleep(2000);
thread.start();
}
}
执行结果
向list添加第1个元素
向list添加第2个元素
向list添加第3个元素
向list添加第4个元素
向list添加第5个元素
Exception in thread "t2" java.lang.RuntimeException
at com.company.WaitNotify$2.run(WaitNotify.java:61)
at java.lang.Thread.run(Thread.java:745)
线程1等待0.5秒,给线程2判断list大小的时间
向list添加第6个元素
向list添加第7个元素
向list添加第8个元素
向list添加第9个元素
向list添加第10个元素
线程1执行结束
当前线程t2收到通知,线程停止..
当前线程t2收到通知,线程并没有马上打印“...收到通知...”。这是因为notify不释放锁,只是通知等待中的线程,加入到竞争队列当中。
为了验证这一问题,将两个线程启动顺序调换,发现线程2一直在等待,不能结束
t2.start();
t.start();
改为
t.start();
t2.start();
这样,由于先执行线程"t1"的话,线程"t1"便先获得锁开始执行,当list2中元素的个数达到5时虽然线程"t"调用了lock.notify();但是由于notify并不释放锁,因此线程"t"继续向下执行,list继续添加元素直到元素的个数达到10,线程"t"结束,这时线程"t2"才获得锁开始执行,但由于list2.size这时已经是10了,再也不会是5了,因此线程"t2"判断list2的元素个数不等于5,于是线程"t2"进入wait状态,线程"t1"已经结束了,没有线程去唤醒线程"t2"了,因此线程"t2"便一直处于等待状态了。
仍有问题:
但是这样的线程之间的通信机制虽然解决了性能消耗问题,但仍然有弊端。因为notify不释放锁,所以线程2必须等线程1完全执行完,线程2才能结束等待继续执行。这失去了线程间通信的实时性。
为解决这个问题,我们使用的工具类是CountDownLatch,该类还有个好处就是不用我们写synchronized关键字修饰了,我们在线程"t2"调用等待方法(countDownLatch.await();),在线程"t1"调用唤醒方法(countDownLatch.countDown();)。而且我们也不必纠结于线程"t1"和"t2"谁先启动谁后启动的问题,谁先启动都可以了。
package com.company;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* Created by BaiTianShi on 2018/8/15.
*/
public class WaitNotify extends Thread{
private volatile static List<String> list = new ArrayList<>();
public void add(){
list.add("memeda");
}
public int getSize(){
return list.size();
}
public static void main(String[] args) throws InterruptedException {
final WaitNotify wn = new WaitNotify();
CountDownLatch co = new CountDownLatch(1);
// Object lock = new Object();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("向list添加第"+(i+1)+"个元素");
wn.add();
if(i == 4){
try {
Thread.sleep(500);
co.countDown();
System.out.println("线程1等待0.5秒,给线程2判断list大小的时间");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("线程1执行结束");
}
},"t1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
if(list.size() != 5){
try {
co.await();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("当前线程"+Thread.currentThread().getName()+"收到通知,线程停止..");
throw new RuntimeException();
}
},"t2");
thread2.start();
Thread.sleep(2000);
thread.start();
}
}
执行结果
向list添加第1个元素
向list添加第2个元素
向list添加第3个元素
向list添加第4个元素
向list添加第5个元素
当前线程t2收到通知,线程停止..
线程1等待0.5秒,给线程2判断list大小的时间
Exception in thread "t2" java.lang.RuntimeException
at com.company.WaitNotify$2.run(WaitNotify.java:60)向list添加第6个元素
at java.lang.Thread.run(Thread.java:745)
向list添加第7个元素
向list添加第8个元素
向list添加第9个元素
向list添加第10个元素
线程1执行结束
由于我的计算机向控制台打印代码时的耗时(也在另一方面说明了countDownLach通信的速度之快),造成部分打印顺序有些错乱,但是我们仍能发现,在第5个元素添加后,线程2马上就开始执行了,实现了实时性。
同时需要说明,countDownLach实例化时需要制定唤醒次数,如 countDownLach cd = new countDownLach(2);就需要各个线程中共执行两次cd.countDown()方法才能达到唤醒线程的目的