调用同步锁的wait()、notify()、notifyAll()进行线程通信

  • 看这个经典的存取款问题,要求两个线程存款,两个线程取款,账户里有余额的时候只能取款,没余额的时候只能存款,存取款金额相同。相当于存取款交替进行,金额相同。
  • 线程间通信,需要通过同一个同步监视器(也就是this或者显式的Object对象)调用通信方法,
  • Object有三个方法,可以用于线程间通信
    • wait()

      • 当前线程等待,并释放同步锁
      • wait():无限期等待
      • wait(long timeout):等待timeout毫秒,
      • wait(long timeout,int nanos):等待timeout毫秒+nanos纳秒,nanos的范围[0,999999]
    • notify()
      • 唤醒该同步监视器上的任意一个线程
      • 只有当前线程调用了wait()方法后,被notify()唤醒的线程才会唤醒
    • notifyAll()
      • 唤醒该同步监视器上的所有线程
      • 只有当前线程调用了wait()方法后,被notify()唤醒的线程才会唤醒
  • 看示例代码:
package testpack;
public class Test1 {
public static void main(String[] args){
Account ac=new Account("A123",0.0);
new Deposit("存款者A",ac,325.0).start(); //这里开启两个存款线程
new Withdraw("取款者甲",ac,325.0).start(); //开启两个取款线程
new Deposit("存款者B",ac,325.0).start();
new Withdraw("取款者乙",ac,325.0).start();
}
}
class Withdraw extends Thread{ //取款任务
private Account account;
private double withdrawAmount;
public Withdraw (String threadName,Account account,double withdrawAmount){
super(threadName);
this.account=account;
this.withdrawAmount=withdrawAmount;
}
public void run(){
for (int i=1;i<=2;i++){ //每个线程循环取款2次
account.withdraw(withdrawAmount);
}
}
}
class Deposit extends Thread{ //存款任务
private Account account;
private double depositAmount;
public Deposit (String threadName,Account account,double depositAmount){
super(threadName);
this.account=account;
this.depositAmount=depositAmount;
}
public void run(){
for (int i=1;i<=2;i++){ //每个线程循环存款2次
account.deposit(depositAmount);
}
}
}
class Account {
private String accountNO;
private double balance; //账户余额
private boolean flag=false; //用于判断该账户是否可以进行存款或取款
public Account(){}
public Account(String no,double balance){
accountNO=no;
this.balance=balance;
}
public double getBalance(){
return balance;
}
public synchronized void withdraw(double amount){ //同步方法,取款
try {
while (!flag){ //标记㈠。特别注意,这里用while进行循环判断,而不是用if-else判断
this.wait(); //flag为false,则不可取款,线程等待,并释放同步锁
}
System.out.println(Thread.currentThread().getName()+"取款:"+amount);
balance-=amount;
System.out.println("取款后,余额为: "+balance);
flag=false; //取款完毕后,将flag切换为false,下一个线程如果是取款线程,则不能取款
System.out.println("---------------上面取款完毕-------------------");
this.notifyAll(); //标记㈢。取款完毕,唤醒其他所有线程
}catch(InterruptedException ex){
ex.printStackTrace();
}
}
public synchronized void deposit(double amount){ //同步方法,存款
try{
while (flag){ //标记㈡。特别注意,这里用while进行循环判断,而不是用if-else判断
this.wait(); //如果flag为true,则不能存款,线程等待并释放同步锁
}
System.out.println(Thread.currentThread().getName()+"存款"+amount);
balance+=amount;
System.out.println("存款后,账户余额为: "+balance);
flag=true; //存款完毕后,将flag切换为true,下一个线程如果是存款线程,则不能存款
System.out.println("---------------上面存款完毕-------------------");
this.notifyAll(); //标记㈣存款完毕后,唤醒其他所有线程 }catch(InterruptedException ex){
ex.printStackTrace();
}
}
}

输出:

存款者A存款325.0

存款后,账户余额为: 325.0

---------------上面存款完毕-------------------

取款者乙取款:325.0

取款后,余额为: 0.0

---------------上面取款完毕-------------------

存款者B存款325.0

存款后,账户余额为: 325.0

---------------上面存款完毕-------------------

取款者甲取款:325.0

取款后,余额为: 0.0

---------------上面取款完毕-------------------

存款者B存款325.0

存款后,账户余额为: 325.0

---------------上面存款完毕-------------------

取款者乙取款:325.0

取款后,余额为: 0.0

---------------上面取款完毕-------------------

存款者A存款325.0

存款后,账户余额为: 325.0

---------------上面存款完毕-------------------

取款者甲取款:325.0

取款后,余额为: 0.0

---------------上面取款完毕-------------------

  • 看上面的输出:存款者A和B,取款者甲和乙分别各进行了2次存款或取款操作,并且交替执行
  • 看上面的标记㈢和㈣
    • 这里只能使用notifyAll(),而不能使用notify()方法,因为可能导致程序阻塞,比如:
    • 存款A线程第一次存款完毕,唤醒一个线程(当然第一次没有线程可供唤醒)并再次执行,wait()。状态:A阻塞+B甲乙就绪
    • 存款B线程试图存款,失败,wait()。状态:AB+甲乙
    • 取款甲线程第一次取款完毕,唤醒存款A线程,并再次执行,wait()。状态:B甲+A乙
    • 取款乙线程试图取款,失败,wait()。状态:B甲乙+A
    • 存款A线程第二次存款完毕,唤醒存款B线程,并再次执行,wait()。状态:甲乙A+B
    • 存款B线程试图存款,失败,wait()。状态:AB甲乙均处于wait()状态
    • 此时,四个线程都处于阻塞状态
  • 再看上面的标记㈠和㈡
    • 上面这段代码主要来源于《疯狂Java讲义 第三版》的“codes\16\16.6\synchronized”目录
    • 原代码用的if-else对flag进行判断,这里存在问题,直接导致不论存款(或取款)成功或失败(即wait),run()方法的循环计数器都会自增1,导致存款(或取款)次数比预计的少,进而导致存款(取款线程已执行完,而存款线程仍在执行)或取款(存款线程已执行完,而取款线程仍在执行)线程阻塞
    • 应当采用while进行循环判断,线程被唤醒之后,应再次进行判断,而不是直接将循环计数器自增,可以保证在每个循环中都成功进行了一次存款

调用Condition对象的的await()、signal()、signalAll()方法实现线程间通信

  • 上面Object的wait()、notify()、notifyAll()方法只能适用于this、显式的Object对象
  • 对于用Lock进行加锁的同步方法,上面的三个方法则不适用,这时候得靠Condition对象的另外三个方法
  • 通过Lock锁的newCondition()方法返回一个Condition对象,然后调用该对象的下面三个方法进行通信
    • await()

      • 类似于wait()方法
      • await(long timeout,int nanos)
      • awaitnanos(long nanosTimeout)
      • awaitUninterruptibly()
      • awaitUntil(Date deadline)
    • signal()
      • 类似于notify()
    • signalAll()
      • 类似于notifyAll()
  • Lock锁的newCondition()方法返回的是ConditionObject对象,这是AbstractQueuedSynchronizer抽象类的一个内部类,该内部类实现了Condition接口
  • 下面用Lock及这三个新方法改写上面的Account类
class Account {
private String accountNO;
private double balance;
private boolean flag=false;
private final ReentrantLock lock=new ReentrantLock(); //创建一把Lock锁
private final Condition cond=lock.newCondition(); //返回Condition对象
public Account(){}
public Account(String no,double balance){
accountNO=no;
this.balance=balance;
}
public double getBalance(){
return balance;
}
public void withdraw(double amount){
lock.lock(); //获取锁并加锁
try {
while (!flag){
cond.await(); //调用Condition对象的await()方法
}
System.out.println(Thread.currentThread().getName()+"取款:"+amount);
balance-=amount;
System.out.println("取款后,余额为: "+balance);
flag=false;
System.out.println("---------------上面取款完毕-------------------");
cond.signalAll();
}catch(InterruptedException ex){
ex.printStackTrace();
}finally{
lock.unlock(); //释放锁
}
}
public void deposit(double amount){
lock.lock();
try{
while (flag){
cond.await();
}
System.out.println(Thread.currentThread().getName()+"存款"+amount);
balance+=amount;
System.out.println("存款后,账户余额为: "+balance);
flag=true;
System.out.println("---------------上面存款完毕-------------------");
cond.signalAll(); }catch(InterruptedException ex){
ex.printStackTrace();
}finally{
lock.unlock();
}
}
}

如果调用了Lock对象的wait()、notify()、notifyAll()方法会怎样?

  • Lock对象也是Object的子类的实例,也拥有这三个方法,按理说调用Lock对象这个同步监视器的该三个方法,也应该能达到通信的目的
  • 改写后,程序输出如下:

存款者A存款325.0Exception in thread "存款者A" Exception in thread "取款者甲" //

存款后,账户余额为: 325.0

---------------上面存款完毕-------------------

取款者甲取款:325.0

取款后,余额为: 0.0

---------------上面取款完毕-------------------

存款者B存款325.0

存款后,账户余额为: 325.0

---------------上面存款完毕-------------------

Exception in thread "存款者B" 取款者乙取款:325.0

取款后,余额为: 0.0

java.lang.IllegalMonitorStateException

---------------上面取款完毕-------------------

at java.lang.Object.notifyAll(Native Method)

at testpack.Account.deposit(Test1.java:86)

at testpack.Deposit.run(Test1.java:39)

Exception in thread "取款者乙" java.lang.IllegalMonitorStateException

at java.lang.Object.notifyAll(Native Method)

at testpack.Account.withdraw(Test1.java:68)

at testpack.Withdraw.run(Test1.java:25)

java.lang.IllegalMonitorStateException

at java.lang.Object.notifyAll(Native Method)

at testpack.Account.withdraw(Test1.java:68)

at testpack.Withdraw.run(Test1.java:25)

java.lang.IllegalMonitorStateException

at java.lang.Object.notifyAll(Native Method)

at testpack.Account.deposit(Test1.java:86)

at testpack.Deposit.run(Test1.java:39)

  • 上面出现了大量的“IllegalMonitorStateException”异常,暂时还分析不了出错的原因

通过阻塞队列实现线程间通信

  • 上面的Account的取款、存款问题,抽象一下:一个Account,两个任务(一个存款、一个取款),每个任务两条线程(但两条线程完成的并不是同一项任务)

  • BlockingQueue是一个阻塞队列接口,它有很多实现类,见下图:来源于《Java疯狂讲义 第三版》

  • 实现类:

    • ArrayBlockingQueue:基于数组实现
    • LinkedBlockingQueue:基于链表实现
    • PriorityBlockingQueue:内部元素按照排序器排序,并非先进先出
    • SynchronousQueue:同步队列,存取交替进行
    • DelayQueue:内部元素实现Delay接口,内部元素按照getDelay()的返回值排序
  • 该接口是Queue的子接口,但并不是作为容器使用,而是作为线程同步工具使用。

  • 当一个线程要往里面put()一个元素时,若队列已满,则线程阻塞

  • 当一个线程从里面take()一个元素时,若队列为空,则线程阻塞

  • 三类方法

    • 在队列尾部插入元素:若队列已满,分别会:

      • add(E e):抛出异常
      • offer(E e):返回false
      • put(E e):阻塞队列
    • 在队列头部取出元素,并删除元素:若队列为空,分别会:
      • remove():抛出异常
      • poll():返回false
      • take():阻塞队列
    • 在队列头部取出元素,但不删除元素:若队列为空,分别会:
      • element():抛出异常
      • peek():返回false
  • 见示例:

package testpack;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; public class Test2 {
public static void main(String[] args){
BlockingQueue<String> bq=new ArrayBlockingQueue<>(1);
new Producer(bq,"生产者A").start();
new Producer(bq,"生产者B").start();
new Consumer(bq,"消费者X").start(); //两个生产者,一个消费者,会产生阻塞
}
}
class Producer extends Thread{
private BlockingQueue<String> bq;
Producer(BlockingQueue bq,String name){
super(name);
this.bq=bq;
}
public void run(){ //run()方法没有被同步,for循环中的代码可能被分开执行
String[] str={"A","B","C"};
for (int i=0;i<3;i++){
System.out.println(getName()+" 准备向阻塞队列中添加元素");
try{
bq.put(str[i%3]);
}catch(InterruptedException ex){
ex.printStackTrace();
}
System.out.println(getName()+"添加元素完成: "+bq);
}
}
}
class Consumer extends Thread{
private BlockingQueue<String> bq;
Consumer(BlockingQueue bq,String name){
super(name);
this.bq=bq;
}
public void run(){
for (int i=0;i<3;i++){
System.out.println(getName()+" 准备从阻塞队列中取出元素");
try{
System.out.println(getName()+"取出元素成功: "+bq.take());
}catch(InterruptedException ex){
ex.printStackTrace();
}
}
}
}
  • 输出结果如下:

生产者A 准备向阻塞队列中添加元素 //线程A被中断,可能在添加成功前或后

生产者B 准备向阻塞队列中添加元素 //线程B可能被中断,可能被阻塞

生产者A添加元素完成: [M] //线程A添加成功

生产者A 准备向阻塞队列中添加元素 //线程A阻塞

消费者X 准备从阻塞队列中取出元素

消费者X取出元素成功: M //线程X取出成功

消费者X 准备从阻塞队列中取出元素 //线程X被阻塞

生产者B添加元素完成: [M] //线程B添加成功

生产者A添加元素完成: [N] //这里之所以连续添加2次,因为X已将元素取出,但没有输出

消费者X取出元素成功: M //X将取出的元素输出

生产者A 准备向阻塞队列中添加元素 //线程A被阻塞或中断

生产者B 准备向阻塞队列中添加元素 //线程B被阻塞或中断

消费者X 准备从阻塞队列中取出元素

消费者X取出元素成功: N //X将取出的元素输出

生产者A添加元素完成: [K] //三次消费已执行结束,生产者线程还在执行,程序阻塞

  • ArrayBlockingQueue内部定义了一把private的ReentrantLock锁,在创建对象时创建锁对象(false策略)
  • 在put()/take()阻塞的时候,会释放ReentrantLock锁对象
  • 该示例存在的问题:生产和消费的run()方法没有被同步,导致输出的信息错乱;如果在run()中设置同步代码块,用bq做锁,则在生产方阻塞的时候导致死锁,暂时还不会解决。

0038 Java学习笔记-多线程-传统线程间通信、Condition、阻塞队列、《疯狂Java讲义 第三版》进程间通信示例代码存在的一个问题的更多相关文章

  1. Java学习笔记-多线程-创建线程的方式

    创建线程 创建线程的方式: 继承java.lang.Thread 实现java.lang.Runnable接口 所有的线程对象都是Thead及其子类的实例 每个线程完成一定的任务,其实就是一段顺序执行 ...

  2. 0036 Java学习笔记-多线程-创建线程的三种方式

    创建线程 创建线程的三种方式: 继承java.lang.Thread 实现java.lang.Runnable接口 实现java.util.concurrent.Callable接口 所有的线程对象都 ...

  3. java学习笔记 --- 多线程(线程安全问题——同步代码块)

    1.导致出现安全问题的原因: A:是否是多线程环境 B:是否有共享数据 C:是否有多条语句操作共享数据 2.解决线程安全问题方法: 同步代码块: synchronized(对象){ 需要同步的代码; ...

  4. 0037 Java学习笔记-多线程-同步代码块、同步方法、同步锁

    什么是同步 在上一篇0036 Java学习笔记-多线程-创建线程的三种方式示例代码中,实现Runnable创建多条线程,输出中的结果中会有错误,比如一张票卖了两次,有的票没卖的情况,因为线程对象被多条 ...

  5. Java多线程学习(五)线程间通信知识点补充

    系列文章传送门: Java多线程学习(二)synchronized关键字(1) Java多线程学习(二)synchronized关键字(2) Java多线程学习(三)volatile关键字 Java多 ...

  6. java多线程与线程间通信

    转自(http://blog.csdn.net/jerrying0203/article/details/45563947) 本文学习并总结java多线程与线程间通信的原理和方法,内容涉及java线程 ...

  7. Java——多线程之线程间通信

    Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线 ...

  8. Java笔记(二十)……线程间通信

    概述 当需要多线程配合完成一项任务时,往往需要用到线程间通信,以确保任务的稳步快速运行 相关语句 wait():挂起线程,释放锁,相当于自动放弃了执行权限 notify():唤醒wait等待队列里的第 ...

  9. Java多线程基础——线程间通信

    在使用多线程的时候,经常需要多个线程进行协作来完成一件事情.在前面两章分析了Java多线程的基本使用以及利用synchronized来实现多个线程同步调用方法或者执行代码块.但上面两章的内容涉及到的例 ...

随机推荐

  1. C# 委托Delegate(一) 基础介绍&用法

    本文是根据书本&网络 前人总结的. 1. 前言 定义&介绍: 委托Delegate是一个类,定义了方法的类型, 使得可以将方法当做另一个方法的参数来进行传递,这种将方法动态地赋给参数的 ...

  2. Atitit 软件架构方法的进化与演进cs bs soa roa  msa  attilax总结

    Atitit 软件架构方法的进化与演进cs bs soa roa  msa  attilax总结 1.1. 软件体系架构是沿着单机到 CS 架构,再到 BS 的三层架构甚至多层架构逐步发展过来的,关于 ...

  3. Atitit 多元化战略 适合我们发展 的核心业务attilax总结

    Atitit 多元化战略 适合我们发展 的核心业务attilax总结 1.1. 历史的大趋势,全球范围内人员的大流动1 1.2. 衣食住行1 1.3. 农村包围城市战略1 1.4. 挪开三座大山(住房 ...

  4. wget 显示"英国中部时间",去掉烦人的刷屏显示

    wget下载文件显示多行,进度条后面显示英国中部时间,非常让人郁闷. 本来英文是eta(Estimated Time of Arrival 预计到达时间),翻译错了,干脆去掉好了. 先要有两个个工具 ...

  5. jQuery的几个应例题、JSON基础

    1.下拉列表取值.赋值 (1)写个下拉列表,如下: <select id="sel"> <option value="山东">山东< ...

  6. VS2010编写WebService与在IIS的发布<之简单讲解>

    工具VS2010,window环境win7 一:Webservice的创建与方法查看调用 1.新建空web应用程序项目 2.新建web服务 3.自动生成 4.直接跑起来,可以看到有2个方法 5.点击H ...

  7. 如何直接在 PC 端获取其它端设备的 UserAgent 信息呢?

    如何直接在 PC 端获取其它端设备的 UserAgent 信息呢 [博主]反骨仔 [原文地址]http://www.cnblogs.com/liqingwen/p/5909615.html 序 希望收 ...

  8. EasyUI中那些不容易被发现的坑——EasyUI重复请求2次的问题

    问题控件:datagrid.combobox.所有能设置url属性的控件 问题版本:1.4.4.1.4.5(之前的版本没测) 问题如图: 重复请求2次,错误代码如图: 错误问题分析:html加载的时候 ...

  9. Java豆瓣电影爬虫——抓取电影详情和电影短评数据

    一直想做个这样的爬虫:定制自己的种子,爬取想要的数据,做点力所能及的小分析.正好,这段时间宝宝出生,一边陪宝宝和宝妈,一边把自己做的这个豆瓣电影爬虫的数据采集部分跑起来.现在做一个概要的介绍和演示. ...

  10. [入门级] 基于 visual studio 2010 mvc4 的图书管理系统开发初步 (二)

    [入门级] 基于 visual studio 2010 mvc4 的图书管理系统开发初步 (二) Date  周六 10 一月 2015 By 钟谢伟 Category website develop ...