JAVA基础知识之多线程——线程通信
传统的线程通信
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;
//if flag = false, means only deposit can be done
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 = false, means only deposit can be done, draw method will be blocked
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 = false, means only draw can be done, deposit method will be blocked
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 生产者准备生产集合元素
7 Thread-8 消费完成: []
8 Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Java]
Thread-6 生产者准备生产集合元素
11 Thread-7 生产完成: [Java, Java]
12 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-6 生产者准备生产集合元素
Thread-5 生产者准备生产集合元素
Thread-7 生产者准备生产集合元素
Thread-8 消费者准备消费集合元素
5 Thread-6 生产完成: []
Thread-6 生产者准备生产集合元素
Thread-5 生产完成: [Java]
Thread-5 生产者准备生产集合元素
Thread-7 生产完成: [Java, Java]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: []
Thread-8 消费者准备消费集合元素
Thread-7 生产完成: [Java, Struts]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Java, Struts]
Thread-8 消费者准备消费集合元素
Thread-8 消费完成: [Struts]
Thread-5 生产完成: [Struts, Struts]
Thread-5 生产者准备生产集合元素
Thread-8 消费者准备消费集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Struts, Struts]
Thread-6 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Struts, Spring]
Thread-6 生产者准备生产集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Spring, Spring]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Spring, Java]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: [Java]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Java, Java]
Thread-6 生产者准备生产集合元素
Thread-8 消费完成: [Java]
Thread-8 消费者准备消费集合元素
Thread-7 生产完成: [Java, Spring]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素
Thread-7 生产完成: [Spring, Java]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Java]
Thread-8 消费者准备消费集合元素
Thread-7 生产完成: [Java, Struts]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-7 生产完成: [Struts, Spring]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素
Thread-7 生产完成: [Spring, Java]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Java]
Thread-8 消费者准备消费集合元素
Thread-7 生产完成: [Java, Struts]
Thread-7 生产者准备生产集合元素
Thread-7 生产完成: [Struts, Spring]
Thread-7 生产者准备生产集合元素
Thread-8 消费完成: [Struts, Spring]
Thread-8 消费者准备消费集合元素
Thread-8 消费完成: [Spring]
Thread-8 消费者准备消费集合元素
Thread-5 生产完成: [Spring, Struts]
Thread-5 生产者准备生产集合元素
Thread-8 消费完成: [Struts]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Struts, Struts]
Thread-6 生产者准备生产集合元素
Thread-8 消费完成: [Struts, Spring]
Thread-8 消费者准备消费集合元素
Thread-6 生产完成: [Struts, Spring]
Thread-6 生产者准备生产集合元素
JAVA基础知识之多线程——线程通信的更多相关文章
- JAVA基础知识之多线程——线程组和未处理异常
线程组 Java中的ThreadGroup类表示线程组,在创建新线程时,可以通过构造函数Thread(group...)来指定线程组. 线程组具有以下特征 如果没有显式指定线程组,则新线程属于默认线程 ...
- JAVA基础知识之多线程——线程池
线程池概念 操作系统或者JVM创建一个线程以及销毁一个线程都需要消耗CPU资源,如果创建或者销毁线程的消耗源远远小于执行一个线程的消耗,则可以忽略不计,但是基本相等或者大于执行线程的消耗,而且需要创建 ...
- JAVA基础知识之多线程——线程同步
线程安全问题 多个线程同时访问同一资源的时候有可能会出现信息不一致的情况,这是线程安全问题,下面是一个例子, Account.class , 定义一个Account模型 package threads ...
- JAVA基础知识之多线程——线程的生命周期(状态)
线程有五个状态,分别是新建(New).就绪(Runnable).运行(Running).阻塞(Blocked)和死亡(Dead). 新建和就绪 程序使用new会新建一个线程,new出的对象跟普通对象一 ...
- java基础知识总结--多线程
1.扩展Java.lang.Thread类 1.1.进程和线程的区别: 进程:每个进程都有自己独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1~n个线程. 线程:同一类线 ...
- JAVA基础知识之多线程——控制线程
join线程 在某个线程中调用其他线程的join()方法,就会使当前线程进入阻塞状态,直到被join线程执行完为止.join方法类似于wait, 通常会在主线程中调用别的线程的join方法,这样可以保 ...
- Java基础知识(多线程和线程池)
新建状态: 一个新产生的线程从新状态开始了它的生命周期.它保持这个状态直到程序 start 这个线程. 运行状态:当一个新状态的线程被 start 以后,线程就变成可运行状态,一个线程在此状态下被认为 ...
- JAVA基础知识之多线程——三种实现多线程的方法及区别
所有JAVA线程都必须是Thread或其子类的实例. 继承Thread类创建线程 步骤如下, 定义Thead子类并实现run()方法,run()是线程执行体 创建此子类实例对象,即创建了线程对象 调用 ...
- JAVA基础知识|进程与线程
一.什么是进程?什么是线程? 操作系统可以同时支持多个程序的运行,而一个程序可以狭义的认为就是一个进程.在一个进程的内部,可能包含多个顺序执行流,而每个执行流就对应一个线程. 1.1.进程 进程:是计 ...
随机推荐
- notpad++安装python插件
1.安装python并添加到环境变量 2.在notpad++ 运行工具下点击运行,输入如下命令: cmd /k python "$(FULL_CURRENT_PATH)" & ...
- Effective C++ 6.继承与面向对象设计
//条款32:确定你的public继承塑模出is-a关系 // 1.public继承意味着is-a的关系,适用在基类上的方法都能用于派生类上. //条款33:避免遮掩继承而来的名称 // 1.在pub ...
- 前端新手分析 AJAX执行顺序,数据走向
我是一名前端的newer 在刚学习AJAX和eJS的时候,对于顺序上面有很大迷惑,现在稍微清楚了一点, 理解不对的地方,还请各位大牛帮助给我指导一下. 总的 服务器和客户端的顺序 一. 除了必要的 ...
- js拖拽换位置,使用数组方法
之前一直需要一个拖拽效果,网上找了些感觉不是不好用,就是写的有些地方让人不太满意,下面贡献一个自己写的.亲测可用,拖动后可互换位置!(带有注释) 方法/步骤 CSS代码部分 <style> ...
- 使用console进行 性能测试 和 计算代码运行时间(转载)
本文转载自: 使用console进行 性能测试 和 计算代码运行时间
- sql查询 所有被锁定的表
--sql查询 所有被锁定的表 select request_session_id spid,OBJECT_NAME(resource_associated_entity_id) tableName ...
- mongo语句优化分析
参考原文:http://www.mongoing.com/eshu_explain3 理想的查询状态由以下两种 普通查询: nReturned=totalKeysExamined & tota ...
- 管理科学与工程 国内核心期刊 国外a刊及SCI
国内: 管理科学与工程: 管理科学学报 A+ (匿名审稿,绝对牛刊,不比一般的SCi期刊的质量差) 系统工程理论与实践 A (实名审稿,关系稿很多,尤其是挂编委的文章很多,但质量尚可)系统工程 ...
- 使用Node.js的socket.io模块开发实时web程序
首发:个人博客,更新&纠错&回复 今天的思维漫游如下:从.net的windows程序开发,摸到nodejs的桌面程序开发,又熟悉了一下nodejs,对“异步”的理解有了上上周对操作系统 ...
- 【python】__future__模块
转自:http://www.jb51.net/article/65030.htm Python的每个新版本都会增加一些新的功能,或者对原来的功能作一些改动.有些改动是不兼容旧版本的,也就是在当前版本运 ...