传统的线程通信

Object提供了三个方法wait(), notify(), notifyAll()在线程之间进行通信,以此来解决线程间执行顺序等问题。

  • wait():释放当前线程的同步监视控制器,并让当前线程进入阻塞状态,直到别的线程发出notify将该线程唤醒。
  • notify():唤醒在等待控制监视器的其中一个线程(随机)。只有当前线程释放了同步监视器锁(调用wait)之后,被唤醒的线程才有机会执行。
  • notifyAll():与上面notify的区别是同时唤醒多个等待线程。

值得注意的是这三个方法是属于Object而不是属于Thread的,但是调用的时候必须用同步监视器来调用,

  • 对于synchronized修饰的同步方法,由于方法所在类对象(this)就是同步监视器,因此可以直接在同步方法中调用这三个方法;
  • 对于同步代码块,synchronized(obj) { ... },则需要用空号钟的obj来调用。

生产者-消费者问题模型

在经典的生产者-消费者问题中,需要使用线程通信来解决。

假设有这么一个场景,有一个线程需要存钱进一个账户,有多个线程需要从这个账户取钱,要求是每次必须先存钱之后才能取钱,而且取钱之后必须存钱,

存钱和取钱不能同时发生两次,而是要保持顺序不变,如何实现这个需求呢。

下面是用同步方法结合线程通信的方式来实现的思路,

  • 首先在Account类中定义两个同步方法,deposit和draw用来确保存款和取款操作的原子性。
  • 在Account类中定义用标识符flag, 由deposit和draw共用。初始值为false,表示只能存款。 如果为false,表示只能取款。
  • 定义一个存款线程类,去调用Account类的同步方法deposit,在deposit中先对flag进行判断,如果不为false,则调用wait阻塞存款线程,等待取款线程发出notice。存款完成之后,将flag改为true.
  • 定义一个取款线程类,去调用Account类的同步方法draw,在draw中先对flag进行判断,如果不为true,则调用wait阻塞取款线程,等待存线程发出notice。取款完成之后,将flag改为false.
  • 定义测试类,同时启动一个(或多个)存款线程进行存款,同时启动多个取款线程去取款,存款(取款)线程之间不会有先后顺序,但是存款和取款直接会有严格的先后顺序,这就解决了生产者消费者问题

下面给出实现代码

在Account类中定义两个同步方法,draw和deposit

package threads.sync;

public class Account {
private String accountNo;
private double balance;
private boolean flag = false;
public Account() {} public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
} public String getAccountNo() {
return accountNo;
} public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
} public double getBalance() {
return balance;
} public void setBalance(double balance) {
this.balance = balance;
} public int hashCode() {
return accountNo.hashCode();
} public boolean equals(Object obj) {
if (this == obj) return true;
if (obj != null && obj.getClass() == Account.class) {
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
} public synchronized void draw(double drawAmount) {
try { if (!flag) {
wait();
} else {
System.out.println(Thread.currentThread().getName()
+ " draw money: " + drawAmount);
balance -= drawAmount;
System.out.println(" "
+ " balance : " + balance);
flag = false;
notifyAll();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
} public synchronized void deposit(double depositAmount) {
try { if (flag) {
wait();
} else {
System.out.println(Thread.currentThread().getName()
+ " deposit money: " + depositAmount);
balance += depositAmount;
System.out.println(" "
+ " balance : " + balance);
flag = true;
notifyAll();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
} }

定义一个存款线程类depositThread

package threads.sync;

public class DepositThread extends Thread {
private Account account;
private double depositAmount;
public DepositThread(String name, Account account, double depositAmount) {
super(name);
this.setAccount(account);
this.setDepositAmount(depositAmount);
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
} public double getDepositAmount() {
return depositAmount;
}
public void setDepositAmount(double depositAmount) {
this.depositAmount = depositAmount;
} public void run() {
for(int i=0 ; i<10; i++) {
account.deposit(depositAmount);
} } }

定义一个取款线程类depositThread

package threads.sync;

public class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.setAccount(account);
this.setDrawAmount(drawAmount);
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public double getDrawAmount() {
return drawAmount;
}
public void setDrawAmount(double drawAmount) {
this.drawAmount = drawAmount;
} public void run() {
for(int i=0 ; i<10; i++) {
account.draw(drawAmount);
}
}
}

下面是测试类,存款线程中会有10次存款,三个取款线程中总共会有30次取款,

package threads.sync;

public class DrawTest {
public static void main(String[] args) {
Account acc = new Account("123456",1000);
new DrawThread("DrawThread", acc, 800).start();
new DepositThread("DepositThread-A",acc,800).start();
new DepositThread("DepositThread-B",acc,800).start();
new DepositThread("DepositThread-C",acc,800).start();
}
}

执行结果,

DepositThread-A        deposit money: 800.0
balance : 1800.0
DrawThread draw money: 800.0
balance : 1000.0
DepositThread-B deposit money: 800.0
balance : 1800.0
DrawThread draw money: 800.0
balance : 1000.0
DepositThread-C deposit money: 800.0
balance : 1800.0
DrawThread draw money: 800.0
balance : 1000.0
DepositThread-C deposit money: 800.0
balance : 1800.0
DrawThread draw money: 800.0
balance : 1000.0
DepositThread-C deposit money: 800.0
balance : 1800.0
DrawThread draw money: 800.0
balance : 1000.0
DepositThread-C deposit money: 800.0
balance : 1800.0
DrawThread draw money: 800.0
balance : 1000.0
DepositThread-C deposit money: 800.0
balance : 1800.0
DrawThread draw money: 800.0
balance : 1000.0
DepositThread-A deposit money: 800.0
balance : 1800.0

从执行结果中可以看到,三个取款线程ABC执行顺序随机,但是总是在存款完成后,才会进行取款操作,而且无论存款还是取款,都不会同时进行两次。

使用condition控制线程通信

如果程序使用lock来同步线程的话,就要使用condition来进行线程通信。

在lock同步线程中,lock 对象就是一个显示的同步监视器,但是这个显示的同步监视器不直接阻塞或者通知线程,而是通过condition——lock对象通过调用newCondition方法返回一个与lock关联的condition对象,由condition对象来控制线程阻塞(await)和发出信号(single)唤醒其他线程。

与synchronized同步线程方式对应的是,conditions方式也提供了三个方法,

await:类似于synchronized隐式同步控制器对象调用的wait方法,可以阻塞当前线程,直到在别的线程中调用了condition的singal方法唤醒该线程。

signal:随机唤醒一个被await阻塞的线程。注意只有在当前线程已经释放lock同步监视器之后,被唤醒的其他线程才有机会执行。

signalAll:与上面类似,但是是唤醒所有线程。

下面用condition的方式来实现前面的银行取钱的例子,只需要修改Account类,改用lock同步线程,condition线程通信,

package threads.sync;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class Account {
private String accountNo;
private double balance;
private boolean flag = false;
//显示定义lock对象
private final Lock lock = new ReentrantLock();
//获取lock对象对应的condition
private final Condition cond = lock.newCondition();
public Account() {} public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
} public String getAccountNo() {
return accountNo;
} public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
} public double getBalance() {
return balance;
} public void setBalance(double balance) {
this.balance = balance;
} public int hashCode() {
return accountNo.hashCode();
} public boolean equals(Object obj) {
if (this == obj) return true;
if (obj != null && obj.getClass() == Account.class) {
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
} public void draw(double drawAmount) {
lock.lock();
try {
//if flag = false, means only deposit can be done, draw method will be blocked
if (!flag) {
//this.wait();
cond.await();
} else {
System.out.println(Thread.currentThread().getName()
+ " draw money: " + drawAmount);
balance -= drawAmount;
System.out.println(" "
+ " balance : " + balance);
flag = false;
//this.notifyAll();
cond.signalAll();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
} public void deposit(double depositAmount) {
lock.lock();
try {
//if flag = false, means only draw can be done, deposit method will be blocked
if (flag) {
//this.wait();
cond.await();
} else {
System.out.println(Thread.currentThread().getName()
+ " deposit money: " + depositAmount);
balance += depositAmount;
System.out.println(" "
+ " balance : " + balance);
flag = true;
//this.notifyAll();
cond.signalAll();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
} }

对比用synchronized方式同步线程的例子,前面例子中是隐式的同步监视器(this)调用wait和notify来通信,

而本例是显示同步监视器(lock)的关联对象(condition)调用await和signal来通信,执行结果与前面一样不再给出。

使用阻塞队列(BlockingQueue)控制线程通信

BlockingQueue是JAVA5提供的一个队列接口,但这个队列并不是用作一个容器,而是作为线程的同步工具。

它可以很好地解决生产者消费者问题,而且比前面提到的两种方式更为灵活,

BlockingQueue的特征是,

当生产者线程试图向BlockingQueue存入元素时,如果队列已满,生产者线程将会阻塞,

当消费者线程试图从BlockingQueue取出元素时,如果队列为空,消费者线程将会阻塞

对比前面线程通信的例子,synchronized同步方法/代码块和lock+condition方式中,都只能控制生产者和消费者按固定顺序执行,

但BlockingQueue则是可以通过集合中的元素个数(商品数量)来控制线程执行顺序,通过调整集合容量可以控制线程切换的条件。

集合(商品)为空时,消费者阻塞,只能执行生产者线程;集合(商品)已满时,生产者阻塞,只能执行消费者线程。

BlockingQueue接口有很多实现类,下面演示最常用的实现类ArrayBlockQueue控制线程通信,

定义一个生产者线程类Producter

package threads.sync;

import java.util.concurrent.BlockingQueue;

public class Producter extends Thread {
private BlockingQueue<String> bq; public Producter(BlockingQueue<String> bq) {
this.bq = bq;
} public void run() {
String[] strArr = new String[] {
"Java",
"Struts",
"Spring"
}; for(int i = 0; i<999999; i++) {
System.out.println(getName()+" 生产者准备生产集合元素");
try {
Thread.sleep(200);
//如果队列已满,线程将阻塞
bq.put(strArr[i % 3]);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+" 生产完成: " + bq);
}
}
}

定义一个消费者类Consumer

package threads.sync;

import java.util.concurrent.BlockingQueue;

public class Consumer extends Thread {
private BlockingQueue<String> bq; public Consumer(BlockingQueue<String> bq) {
this.bq = bq;
} public void run() { while (true) {
System.out.println(getName()+" 消费者准备消费集合元素");
try {
Thread.sleep(200);
//如果队列已空,线程将阻塞
bq.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+" 消费完成: " + bq);
}
}
}

在测试类中,定义一个容量为2的阻塞集合,

启动三个生产者线程, 每个线程都在不停生产商品,存入阻塞队列中,

启动一个消费者线程,每个线程也在不停从阻塞队列中取出商品,

package threads.sync;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; public class BlockingQueueTest {
public static void main(String[] args) {
BlockingQueue<String> bq = new ArrayBlockingQueue<String>(2);
new Producter(bq).start();
new Producter(bq).start();
new Producter(bq).start();
new Consumer(bq).start();
}
}

执行结果,从执行结果中可以看到,只要集合中有元素且集合没有满,那么生产者和消费者线程都有机会得到执行,具体谁有机会要看谁抢到CPU执行片,

但是当集合空了的时候,例如第7行(Thread-8 消费完成: []),接着又有一个消费者线程执行,但是因此集合为空而阻塞了,此时只有生产者线程能执行,

当集合满了的时候,例如第11行(Thread-7 生产完成: [Java, Java]),接着又有一个生产者线程执行,但是因为集合已满而阻塞了,此时只有消费者线程能执行。

Thread-5 生产者准备生产集合元素
Thread-6 生产者准备生产集合元素
Thread-7 生产者准备生产集合元素
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Java]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: []
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Java]
Thread-6 生产者准备生产集合元素
Thread-7 生产完成: [Java, Java]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Java]
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Java, Struts]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Struts, Struts]
Thread-6 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-7 生产完成: [Struts, Struts]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Struts, Spring]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Spring, Spring]
Thread-6 生产者准备生产集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素

上面的例子来自李刚的疯狂JAVA, 但个人认为并不是太好,因为无论在生产者还是消费者线程中,打印bq操作前后的两段日志并不是原子操作,这会导致打印的日志不准确,

例如下面的运行结果,从第5行看到(Thread-6 生产完成: []),刚刚执行完一个生产者线程中的入队操作,但是打印队列却是空的,原因就在于在打印两行日志期间,消费者线做了取元素的操作。

Thread-5 生产者准备生产集合元素
Thread-6 生产者准备生产集合元素
Thread-7 生产者准备生产集合元素
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Java]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: []
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Java]
Thread-6 生产者准备生产集合元素
Thread-7 生产完成: [Java, Java]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Java]
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Java, Struts]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Struts, Struts]
Thread-6 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-7 生产完成: [Struts, Struts]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Struts, Spring]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Spring, Spring]
Thread-6 生产者准备生产集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素

java 多线程间通信(二)的更多相关文章

  1. Java 多线程间通信

    JDK 1.5 以后, 将同步和锁封装成了对象, 并将操作锁的隐式方法定义到了该对象中, 将隐式动作变成了显示动作. Lock 接口 Lock 接口, 位于 java.util.concurrent. ...

  2. Java多线程间通信-解决安全问题、等待唤醒机制

    /*1.增加一个知识点一个类怎么在所有的类中,让其它类来共同修改它的数据呢?可以用单例设计模式可以用静态可以在其它类中做一个构造函数,接受同一个对象,这样就可以实现对象 2.状态选择可以用数字0 1 ...

  3. java 多线程间通信(一)

    synchronized同步 package com.test7; public class Run { public class MyObject { private int a; public M ...

  4. Java线程间通信-回调的实现方式

    Java线程间通信-回调的实现方式   Java线程间通信是非常复杂的问题的.线程间通信问题本质上是如何将与线程相关的变量或者对象传递给别的线程,从而实现交互.   比如举一个简单例子,有一个多线程的 ...

  5. Java多线程学习(二)synchronized关键字(2)

    转载请备注地址:https://blog.csdn.net/qq_34337272/article/details/79670775 系列文章传送门: Java多线程学习(一)Java多线程入门 Ja ...

  6. (十一)boost库之多线程间通信

    (十一)boost库之多线程间通信 1.互斥锁 在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性.每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一 ...

  7. “全栈2019”Java多线程第二十二章:饥饿线程(Starvation)详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  8. “全栈2019”Java多线程第十二章:后台线程setDaemon()方法详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  9. Java多线程学习(二)synchronized关键字(1)

    转载请备注地址: https://blog.csdn.net/qq_34337272/article/details/79655194 Java多线程学习(二)将分为两篇文章介绍synchronize ...

随机推荐

  1. 11-基于CPCI的中频功率放大收发板

    1.板卡参数介绍 无线输入口 无线发射口 1M~3GHZ,可调,步进100HZ(非跳频模式) 功率:≤﹢10±2.5 dBm 收发通道数 收发各1通道/板 中频输入输出 70MHz, 5MHz/30M ...

  2. python如何获取变量的变量名

    假设现在存在一个值为1变量名为a的变量,如何通过一个函数获取该变量的变量名a? 上面这个需求来源于某群友的一个要求,希望能有一个这样的函数来方便打印. 这个需求很扯淡啊,为什么不用格式化输出?它回复到 ...

  3. Android】Retrofit网络请求参数注解,@Path、@Query、@QueryMap...(转)

    对Retrofit已经使用了一点时间了,是时候归纳一下各种网络请求的service了. 下面分为GET.POST.DELETE还有PUT的请求,说明@Path.@Query.@QueryMap.@Bo ...

  4. Vue列表渲染:一个取值范围的v-for

    v-for 也可以取整数.在这种情况下,它将重复多次模板. 例子 <div id="app"> <span v-for="n in 10"&g ...

  5. java this的应用

    package java04; /* * 当方法的局部变量和类的成员变量重名时,会根据“就近原则”,优先使用局部变量. * 如果需要访问奔雷中的成员变量,需要使用格式: this.成员变量名 * * ...

  6. ltp-ddt lmbench

    ltp-ddt lmbench args='' # getopt fails, set help optionif [ $? -ne 0 ] ; then        H="help&qu ...

  7. PHP计算经纬度在百度多边形区域内

    最近做一个项目需要使用到区域,并且要判断当前的经纬度是否在区域内,已便对应业务需求变化.废话不多说直接上代码: /** * 验证区域范围 * @param array $coordArray 区域 * ...

  8. 自建云存储:Nextcloud vs. ownCloud vs. Seafile

    Self-hosted Cloud Storage: Nextcloud vs. ownCloud vs. Seafile By Ashutosh KS in Hosting. Updated on ...

  9. boost timer

    Boost.Timer provides clocks to measure code performance. At first, it may seem like this library com ...

  10. boost algorithm

    BOost Algorithm provides algorithms that complement the algorithms from the standard library. Unlike ...