当程序在系统中运行时,线程的调度具有一定的随机性,程序通常无法准确的控制线程的轮换执行,我们可以通过一些机制来保证线程协调运行。线程之间的协调运行就称为通信。
>传统线程通信
假设系统中有两个线程,一个代表存款者,一个代表取款者。系统对他们有特殊要求,即要求存款者与取款者不断地重复存款、取钱的动作,而且要求每当存款者将钱存入指定账户后取款者立即取出钱,不允许存款者连续两次存钱,也不允许取钱者连续两次取钱。
为实现此功能,可以借助Object类提供的wait()、notify()和notifyAll()3个方法,这3个方法必须由同步监视器对象来调用,同步监视器可以分以下两种情况:
三个方法解释如下:
这样看来,是同步监视器通过这三个方法来告知,并发执行的线程 “你先等一等” 或者 “你们醒一醒,等这个线程释放了(推出synchronized代码区),某个线程就可以执行了”:
( 如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。 )
程序中可以通过一个旗标来标识账户中是否已经有存款,当旗标为false时,标识账户中没有存款,存款者线程可以向下执行,当存款者把钱存入账户后,将旗标设置为true,表调用notify()或者notifyAll()方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为true就调用wait()方法让线程等待。
当旗标为true时,表明账户中已经有了存款,则取款者线程就可以向下执行,当取款者把钱从账户中取出后,将镖旗设置为false,并调用notify()或者notifyAll()方法来唤醒其他线程;当取钱者线程进入线程体后,入股旗标为false就调用wait()方法让该线程等待。
本程序为Account类提供了draw()和deposit()两个方法,分别对应该账户的取钱、存款操作。因为这两个方法可能需要并发修改Account对象的balance成员变量,因此这两个方法都使用synchrozied修饰进行同步控制,除此之外这两个方法还使用wait()、notifyAll来控制线程的协作。
账户(同步监视器):
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 boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public synchronized void drawMoney() {
if (flag) {
System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:800");
this.setBalance(this.balance - 800);
System.out.println("当前余额为:" + this.getBalance());
this.flag = false;
this.notifyAll();// 唤醒在此同步监视器(account对象)上等待的其他线程
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void deposit() {
if (flag) {
try {
this.wait();// this指account对象,通过account对象的wait方法让线程进入等待
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + "存钱成功:800!");
this.setBalance(this.balance + 800);
System.out.println("当前余额为:" + this.getBalance());
this.flag = true;
this.notifyAll();
}
}
}
取钱线程:
public class DrawThread extends Thread{
private Account account;
public DrawThread(Account account) {
super();
this.account = account;
}
public void run(){
for (int i = 0; i < 20; i++) {
this.account.drawMoney();
}
}
}
存钱线程:
public class DepositThread extends Thread {
private Account account;
public DepositThread(Account account) {
super();
this.account = account;
}
public void run(){
for (int i = 0; i < 20; i++) {
this.account.deposit();
}
}
}
测试类:
public class Run {
public static void main(String[] args) {
Account account=new Account("123456", 0);
new DrawThread(account).start();
new DepositThread(account).start();
}
}
运行结果:
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
Thread-0取钱成功!吐出钞票:800
当前余额为:0.0
Thread-1存钱成功:800!
当前余额为:800.0
>锁池(monitor)和等待池(waitset)
锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.
>永远不要在循环的外面调用wait():在一个合格的多线程程序中,这是必须的
synchronized (obj) {
while ()
obj.wait();
... // Perform action appropriate to condition
}
这样做的意思是只要还没有达到,该线程就一直等待,直到满足条件,可以避免不必要的唤醒——“虚假唤醒”。
比如调用notifyAll,唤醒对应锁上的所有线程,那所有wait的线程,就从wait方法返回了,但自己程序里的逻辑,是不是期望这种结果呢?所以针对每个wait,还要重复判断一下,确定此时是不是真的该醒来了,所以用for/while,而不是if判断。
>使用Condition控制线程通信
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()、notifyAll()方法进行线程通信了。
当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。
Condition将同步监视器方法(wait、notify、notifyAll)分解成截然不同的对象,以便通过将这些对象与Lock组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。
Condition实例被绑定在一个Lock对象上。要获得特定Lock对象的Condition实例,调用Lock对象的newCondition()方法即可。Condition类提供了如下三个方法。
将上面的代码改写:
public class Account {
private String accountNO;
private double balance;
private boolean flag = false;
private final ReentrantLock lock = new ReentrantLock();
private 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 boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public void drawMoney() {
lock.lock();
try {
if (flag) {
System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票:800");
this.setBalance(this.balance - 800);
System.out.println("当前余额为:" + this.getBalance());
this.flag = false;
cond.signalAll();
} else {
try {
cond.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void deposit() {
lock.lock();
try {
if (flag) {
try {
cond.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + "存钱成功:800!");
this.setBalance(this.balance + 800);
System.out.println("当前余额为:" + this.getBalance());
this.flag = true;
cond.signalAll();
}
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
>阻塞队列控制线程通信
Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程同步的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞,等待队列可用;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞,等待队列非空。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
BlockingQueue提供如下两个支持阻塞的方法:
BlockingQueue继承了Queue接口,当然也可使用Queue接口中的方法。这些方法归纳起来可分为如下三类:
BlockingQueue包含如下5个实现类:
测试代码:一个生产线程,一个消费线程,并发操作阻塞队列:
public class MyBlockingQueue extends ArrayBlockingQueue{
public MyBlockingQueue(int capacity) {
super(capacity);
}
public Double take() throws InterruptedException{
Double resule =super.take();
System.out.println(Thread.currentThread().getName()+"取走集合元素!");
return resule;
}
public void put(Double e) throws InterruptedException{
super.put(e);
System.out.println(Thread.currentThread().getName()+"放入新的集合元素!");
}
}
存入:
public class DepositThread extends Thread {
private BlockingQueue queue;
public DepositThread(String name, BlockingQueue queue) {
super(name);
this.queue = queue;
}
public void run() {
while (true) {
try {
Thread.sleep(500);
this.queue.put(new Double(800));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
取出:
public class ATMThread extends Thread {
private BlockingQueue queue;
public ATMThread(String name, BlockingQueue queue) {
super(name);
this.queue = queue;
}
public void run() {
while (true) {
try {
Thread.sleep(2000);
this.queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyBlockingQueue myque = new MyBlockingQueue(10);
new ATMThread("取出线程", myque).start();
new DepositThread("存入者", myque).start();
}
}