第五章:多线程通信—wait和notify

第五章:多线程通信—wait和notify第五章:多线程通信—wait和notify

大家好,又见面了,我是你们的朋友全栈君。

   线程通信概念:线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体,线程间的通信就成为整体的必用方式之一。当线程存在通信指挥,系统间的交互性会更大,在提高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.xiaoyexinxin.ThreadLearn;

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() {  
          
            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() {  
          
            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.xiaoyexinxin.ThreadLearn.ListAdd1$2.run(ListAdd1.java:39)
	at java.lang.Thread.run(Unknown Source)
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..

     那么第一问便是:请使用wait和notify来改善上面的代码

        使用wait和notify第一版代码如下,可以看到加上了synchronized关键字并且使用了wait和notify,特别需要注意的是,线程的启动顺序是先启动t2然后启动t1。

package com.xiaoyexinxin.ThreadLearn;

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  list2 = new ListAdd1();  
	       //1.实例化出来一个lock  
	       //当使用wait和notify的时候,一定要配合着synchronized关键字去使用  
	       final Object lock = new Object();  
	         
	       Thread t1 = new Thread(new Runnable() {  
	          
	            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() {  
	          
	            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();   //让t2线程先执行
	       t1.start();  
	         
	   }  
}

执行结果:

当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
已经发出通知
eee
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程t2收到通知,线程停止..
Exception in thread "t2" #######3333333333333333
java.lang.RuntimeException
	at com.xiaoyexinxin.ThreadLearn.ListAdd1$2.run(ListAdd1.java:58)
	at java.lang.Thread.run(Unknown Source)

出现这样的结果是由于wait是释放锁的,而notify是不释放锁的,线程”t2″先执行,一判断发现list2.size不等于5,于是乎线程”t2″进入wait状态,释放了锁,这时线程”t1″便获得锁开始执行,当线程”t1″添加5个元素后判断发现list2.size是5了,于是乎打出了”已发出通知”的日志,lock.notify去唤醒”t2″线程,但是由于notify并不释放锁,因此线程”t1″依然拿着锁执行后面的代码,直到线程”t1″执行完后,线程”t2″才获得锁开始执行,于是乎打印出”收到通知,线程停止”并抛出异常。


就是如果让线程”t1″先执行,线程”t2″后执行,会是什么结果呢?我们把两个线程的启动顺序调换,如下所示。

 t1.start();

      t2.start();  //让t2线程先执行

  运行结果如下,发现线程”t1″执行完了,但是线程”t2″一直在等待,无法结束。

当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
已经发出通知
eee
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..

#######3333333333333333

 为什么会出现上面的情况呢?这是由于先执行线程”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.xiaoyexinxin.ThreadLearn;

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 ListAdd3  list2 = new ListAdd3();  
       //这是并发包下的一个非常好用的工具类,实例化时的参数1代表需要调用几次  
       //countDownLatch.countDown();才能叫醒,1就是调用1次即可,2就要调2次才行,  
       //我们一般都用1就行了  
       final CountDownLatch countDownLatch = new CountDownLatch(1);  
         
       Thread t1 = new Thread(new Runnable() {  
          
            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() {  
          
            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();   
   }  

}

可以看到线程”t2″实时的接收到了通知(我们不必纠结于”t2″线程停止前打印了6条”t1″添加元素的信息,这是打印的顺序的问题,我们看到后面有四条添加元素的信息就对了)。可见这个工具类还是非常好用的。

当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
已经发出通知
Exception in thread "t2" java.lang.RuntimeException
	at com.xiaoyexinxin.ThreadLearn.ListAdd3$2.run(ListAdd3.java:54)
	at java.lang.Thread.run(Unknown Source)
当前线程:t1添加了一个元素..
当前线程t2收到通知,线程停止..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..
当前线程:t1添加了一个元素..

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/106147.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • Java| 编译和反编译

    Java| 编译和反编译原文链接:http://www.yveshe.com/articles/2018/05/01/1525172129089.html什么是编程语言?在介绍编译和反编译之前,我们先来简单介绍下编程语言(ProgrammingLanguage)。编程语言(ProgrammingLanguage)分为低级语言(Low-levelLanguage)和高级语言(High-levelLa…

  • 微信小程序+php 授权登陆,完整代码

    微信小程序+php 授权登陆,完整代码先上图实现流程:1、授权登陆按钮和正文信息放到了同一个页面,未授权的时候显示登陆按钮,已授权的时候隐藏登陆按钮,显示正文信息,当然也可以授权和正文分开成两个页面,在授权页面的onload里判断是否

  • MySQL主从复制配置[通俗易懂]

    MySQL主从复制配置[通俗易懂]MySQL主从复制及读写分离一、MySQL复制的应用常见场景 读写分离,提高查询访问性能,有效减少主数据库访问压力。 实时灾备,主数据库出现故障时,可快速切换到从数据库。 数据汇总,可将多个主数据库同步汇总到一个数据库中,方便数据统计分析。 二、MySQL主从复制原理介绍1、MySQL异步和半同步复制传统的MySQL复制提供了一种简单的主-从复制方法。有一个主,以及一个或多个从。主节点执行和提交事务,然后将它们(异步地)发送到从节点,以重新…

  • c按位取反运算符_取反和按位取反

    c按位取反运算符_取反和按位取反介绍二进制是计算机运行和存储数据的基础,按位取反(以下称“取反”)也就是基于二进制进行的一个操作。所不同的是,在完成按位取反之后,还需要转换为“原码”。(人类可能无法接受二进制表示而更倾向于十进制)正数取反取反就是将二进制表示的数字中的0变为1,1变为0。其实原理很简单,但我一开始很难理解首先将数据转换为二进制表示(这里用整数),以10为例子。1.10用二进制表示就是0000101

  • 前端和后端开发的异同点_后端开发需要掌握什么技术

    前端和后端开发的异同点_后端开发需要掌握什么技术昨天有朋友问我你写了这么多年的代码,你到底是前端开发人员还是后端开发人员?我被这个问题给愣住了,问题不在前端和后端,而在于这么多年我还是一个开发人员。但我不在乎这件事情,因为这么多年了,我发现我对写代

  • freeswitch呼叫中心开发

    freeswitch呼叫中心开发开发freeswitch呼叫中心1、配置ivr2、启用mod_callcenter3、开发websocker接口,通过esl接口,发送callcenter_config命令给fs4、开发客户端页面,注册,注销,就绪,置忙等接口5、开发来电弹屏,通过客户端读取redis参数实现freeswitch的呼叫中心模块很方便的就可以让用户体验这种呼叫中心模式,包含了很多功能,具体参数的配置在使用中自行摸索。编译安装freeswitch时需要开启mod_callcenter、mod_fifo的编译,之

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号