0038 Java学习笔记-多线程-传统线程间通信、Condition、阻塞队列、《疯狂Java讲义 第三版》进程间通信示例代码存在的一个问题
调用同步锁的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()唤醒的线程才会唤醒
- wait()
- 看示例代码:
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()
- await()
- 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讲义 第三版》进程间通信示例代码存在的一个问题的更多相关文章
- Java学习笔记-多线程-创建线程的方式
创建线程 创建线程的方式: 继承java.lang.Thread 实现java.lang.Runnable接口 所有的线程对象都是Thead及其子类的实例 每个线程完成一定的任务,其实就是一段顺序执行 ...
- 0036 Java学习笔记-多线程-创建线程的三种方式
创建线程 创建线程的三种方式: 继承java.lang.Thread 实现java.lang.Runnable接口 实现java.util.concurrent.Callable接口 所有的线程对象都 ...
- java学习笔记 --- 多线程(线程安全问题——同步代码块)
1.导致出现安全问题的原因: A:是否是多线程环境 B:是否有共享数据 C:是否有多条语句操作共享数据 2.解决线程安全问题方法: 同步代码块: synchronized(对象){ 需要同步的代码; ...
- 0037 Java学习笔记-多线程-同步代码块、同步方法、同步锁
什么是同步 在上一篇0036 Java学习笔记-多线程-创建线程的三种方式示例代码中,实现Runnable创建多条线程,输出中的结果中会有错误,比如一张票卖了两次,有的票没卖的情况,因为线程对象被多条 ...
- Java多线程学习(五)线程间通信知识点补充
系列文章传送门: Java多线程学习(二)synchronized关键字(1) Java多线程学习(二)synchronized关键字(2) Java多线程学习(三)volatile关键字 Java多 ...
- java多线程与线程间通信
转自(http://blog.csdn.net/jerrying0203/article/details/45563947) 本文学习并总结java多线程与线程间通信的原理和方法,内容涉及java线程 ...
- Java——多线程之线程间通信
Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线 ...
- Java笔记(二十)……线程间通信
概述 当需要多线程配合完成一项任务时,往往需要用到线程间通信,以确保任务的稳步快速运行 相关语句 wait():挂起线程,释放锁,相当于自动放弃了执行权限 notify():唤醒wait等待队列里的第 ...
- Java多线程基础——线程间通信
在使用多线程的时候,经常需要多个线程进行协作来完成一件事情.在前面两章分析了Java多线程的基本使用以及利用synchronized来实现多个线程同步调用方法或者执行代码块.但上面两章的内容涉及到的例 ...
随机推荐
- 最全的Windows Azure学习教程汇总
Windows Azure 是微软基于云计算的操作系统,能够为开发者提供一个平台,帮助开发可运行在云服务器.数据中心.Web 和 PC 上的应用程序. Azure 是一种灵活和支持互操作的平台,能够将 ...
- 【Win 10 应用开发】UDP广播
我们知道,对于UDP协议的通信,除了可以用来聊天外,可以发送广播数据.只要向广播地址的某个端口发送数据就可以进行广播,子网中只要监听该端口的socket就能收到广播消息. 最简单的方法就是向255.2 ...
- 从零开始编写自己的C#框架(23)——上传组件使用说明
文章导航 1.前言 2.上传组件功能说明 3.数据库结构 4.上传配置管理 5.上传组件所使用到的类 6.上传组件调用方法 7.效果演示 8.小结 1.前言 本系列所使用的是上传组件是大神July开发 ...
- 2、C#核心编程结构
本学习主要参考Andrew Troelsen的C#与.NET4高级程序设计,这小节主要述说以下几个东西: Hello World的Main方法: 利用VS2010新建一个控制台应用程序Hello W ...
- 【Java心得总结一】Java基本类型和包装类型解析
说到数据类型这个问题是一个基本的不能再基本的问题,我们当初编程入门第一课一般就是讲数据类型,而今天我想记录的是一个在Java中容易忽略的问题,即基本类型和包装类型. 一.基本类型出现的原因 我们都知道 ...
- Android keycode列表
整理备忘! 基本按键 KEYCODE_0 按键'0' 7 KEYCODE_1 按键'1' 8 KEYCODE_2 按键'2' 9 KEYCODE_3 按键'3' 10 KEYCODE_4 按键'4' ...
- jQuery-1.9.1源码分析系列(十六)ajax——jsonp原理
json jsonp 类型 "json": 把响应的结果当作 JSON 执行,并返回一个JavaScript对象.如果指定的是json,响应结果作为一个对象,在传递给成功处理函数 ...
- android 模拟2048
利用节日休息时间在ANDROID上进行学习并模拟2048游戏. 效果如下图: 制作思路: 1.画出2048游戏主界面,根据手机屏幕宽高度进行计算并画出每个方块的大小. @Override protec ...
- Git 相关总结
Git 优秀在线教程 廖雪峰-史上最浅显易懂的Git教程!
- windows7命令帮助大全
有关某个命令的详细信息,请键入 HELP 命令名ASSOC 显示或修改文件扩展名关联.ATTRIB 显示或更改文件属性.BREAK 设置或清除扩展式 CTRL+C 检查.BCDEDIT 设置启动数据库 ...