您的位置:首页 > 业界新闻

学习互联网架构第五课(多线程通信---wait和notify)

2017-06-14 10:18 183 查看
        线程通信概念:线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体,线程间的通信就成为整体的必用方式之一。当线程存在通信指挥,系统间的交互性会更大,在提高CPU利用率的同时还会使开发人员对线程任务在处理的过程中进行有效的把控与监督。

        使用wait/notify方法实现线程间的通信。(注意这两个方法都是object的类的方法,换句话说java为所有的对象都提供了这两个方法)

        1.wait和notify必须配合synchronized关键字使用

        2.wait方法释放锁,notify方法不释放锁。

        下面我们来看一道阿里巴巴的面试题,我们看如下所示类代码,代码的意思是在ListAdd1类中添加了一个add方法,该方法向list中添加字符串,size方法返回list的大小。线程"t1"调用10次add方法,每调用一次休眠0.5秒(这样线程"t2"便有时间来判断list的大小),线程"t2"有个死循环不停的去判断list的大小是否到5了,如果到5了,那么就记录日志并抛出异常结束死循环。

package com.internet.thread;

import java.util.ArrayList;
import java.util.List;

public class ListAdd1 {
private volatile static List list = new ArrayList();

public void add(){
list.add("winner");
}
public int size(){
return list.size();
}

public static void main(String[] args){
final ListAdd1 list1 = new ListAdd1();
Thread t1 = new Thread(new Runnable() {

@Override
public void run() {
try {
for(int i=0;i<10;i++){
list1.add();
System.out.println("当前线程:"+Thread.currentThread().getName()+"添加了一个元素..");
Thread.sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
}
},"t1");

Thread t2 = new Thread(new Runnable() {

@Override
public void run() {
while(true){
if(list.size() == 5){
System.out.println("当前线程收到通知:"+Thread.currentThread().getName()+"list size=5线程停止..");
throw new RuntimeException();
}
}

}
},"t2");

t1.start();
t2.start();
}
}
         我们来看执行结果,可以看到结果与我们所设计的一致,但是这种设计很不好,因为它需要线程"t2"不停的去判断list的大小,这是很耗性能的。
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程收到通知:t2list size=5线程停止..
Exception in thread "t2" java.lang.RuntimeException
at com.internet.thread.ListAdd1$2.run(ListAdd1.java:41)
at java.lang.Thread.run(Thread.java:745)
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..        那么第一问便是:请使用wait和notify来改善上面的代码
        使用wait和notify第一版代码如下,可以看到加上了synchronized关键字并且使用了wait和notify,特别需要注意的是,线程的启动顺序是先启动t2然后启动t1。

package com.internet.thread;

import java.util.ArrayList;
import java.util.List;

public class ListAdd2 {
private volatile static List list = new ArrayList();

public void add(){
list.add("winner");
}
public int size(){
return list.size();
}

public static void main(String[] args){
final ListAdd2 list2 = new ListAdd2();
//1.实例化出来一个lock
//当使用wait和notify的时候,一定要配合着synchronized关键字去使用
final Object lock = new Object();

Thread t1 = new Thread(new Runnable() {

@Override
public void run() {
try {
//线程t1和t2一定要用同一把锁,就是都使用lock
synchronized (lock) {
for(int i=0;i<10;i++){
list2.add();
System.out.println("当前线程:"+Thread.currentThread().getName()+"添加了一个元素..");
Thread.sleep(500);
if(list2.size() == 5){
System.out.println("已经发出通知");
lock.notify();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
},"t1");

Thread t2 = new Thread(new Runnable() {

@Override
public void run() {
synchronized (lock) {
if(list2.size() != 5){
try {
lock.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("当前线程"+Thread.currentThread().getName()+"收到通知,线程停止..");
throw new RuntimeException();
}

}
},"t2");

t2.start();
t1.start();

}
}
       下面我们来执行下上面的代码,结果如下,出现这样的结果是由于wait是释放锁的,而notify是不释放锁的,线程"t2"先执行,一判断发现list2.size不等于5,于是乎线程"t2"进入wait状态,释放了锁,这时线程"t1"便获得锁开始执行,当线程"t1"添加5个元素后判断发现list2.size是5了,于是乎打出了"已发出通知"的日志,lock.notify去唤醒"t2"线程,但是由于notify并不释放锁,因此线程"t1"依然拿着锁执行后面的代码,直到线程"t1"执行完后,线程"t2"才获得锁开始执行,于是乎打印出"收到通知,线程停止"并抛出异常。
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
已经发出通知
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程t2收到通知,线程停止..
Exception in thread "t2" java.lang.RuntimeException
at com.internet.thread.ListAdd2$2.run(ListAdd2.java:58)
at java.lang.Thread.run(Thread.java:745)         有的同学可能会有疑问,就是如果让线程"t1"先执行,线程"t2"后执行,会是什么结果呢?我们把两个线程的启动顺序调换,如下所示。
t1.start();
t2.start();          运行结果如下,发现线程"t1"执行完了,但是线程"t2"一直在等待,无法结束。



        为什么会出现上面的情况呢?这是由于先执行线程"t1"的话,线程"t1"便先获得锁开始执行,当list2中元素的个数达到5时虽然线程"t1"调用了lock.notify();但是由于notify并不释放锁,因此线程"t1"继续向下执行,list2继续添加元素直到元素的个数达到10,线程"t1"结束,这时线程"t2"才获得锁开始执行,但由于list2.size这时已经是10了,再也不会是5了,因此线程"t2"判断list2的元素个数不等于5,于是线程"t2"进入wait状态,线程"t1"已经结束了,没有线程去唤醒线程"t2"了,因此线程"t2"便一直处于等待状态了。

        我们可以看到,当前这种处理方式(线程t2先执行,t1后执行)不好,因为线程"t2"要等到线程"t1"执行完毕才能接收到通知,这显然不符合实时性的要求。

        这时请看该题的第二问:既然上面那种方式不合理,请用java.util.concurrent包下的一个工具来实现实时的接收通知。答案如下:我们使用的工具类是CountDownLatch,该类还有个好处就是不用我们写synchronized关键字修饰了,我们在线程"t2"调用等待方法(countDownLatch.await();),在线程"t1"调用唤醒方法(countDownLatch.countDown();)。而且我们也不必纠结于线程"t1"和"t2"谁先启动谁后启动的问题,谁先启动都可以了。

package com.internet.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class ListAdd3 {
private volatile static List list = new ArrayList();

public void add(){
list.add("winner");
}
public int size(){
return list.size();
}

public static void main(String[] args){
final ListAdd2 list2 = new ListAdd2();
//这是并发包下的一个非常好用的工具类,实例化时的参数1代表需要调用几次
//countDownLatch.countDown();才能叫醒,1就是调用1次即可,2就要调2次才行,
//我们一般都用1就行了
final CountDownLatch countDownLatch = new CountDownLatch(1);

Thread t1 = new Thread(new Runnable() {

@Override
public void run() {
try {
for(int i=0;i<10;i++){
list2.add();
System.out.println("当前线程:"+Thread.currentThread().getName()+"添加了一个元素..");
Thread.sleep(500);
if(list2.size() == 5){
System.out.println("已经发出通知");
countDownLatch.countDown();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
},"t1");

Thread t2 = new Thread(new Runnable() {

@Override
public void run() {
if(list2.size() != 5){
try {
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("当前线程"+Thread.currentThread().getName()+"收到通知,线程停止..");
throw new RuntimeException();
}
},"t2");

t1.start();
t2.start();
}
}
        我们运行上面的这个main方法,结果如下图所示,可以看到线程"t2"实时的接收到了通知(我们不必纠结于"t2"线程停止前打印了6条"t1"添加元素的信息,这是打印的顺序的问题,我们看到后面有四条添加元素的信息就对了)。可见这个工具类还是非常好用的。



        
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: