当线程在运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,为了保证线程协调运行,java提供了一些线程通信机制。
一、等待唤醒机制
Object类提供了wait()、notify()、notifyAll()三个方法来实现线程通信机制,但这三个方法必须由同步监视器对象来调用,这样可以分成两种情况:
- 对于使用synchronized修饰的同步方法,因为该类的默认实例(this)是同步监视器所以在同步方法中可以直接调用这三个方法。
- 对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以在必须使用该对象调用这三个方法。
关于三个方法的解释:
- wait():导致当前线程等待,直到在其他线程调用该同步监视器的 notify()方法或notifyAll() 方法来唤醒该线程。调用wait()方法的当前线程会释放对该同步监视器的锁定。
- notify():唤醒在此监视器上等待的单个线程。如果所有线程都在同步监视器对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃此同步监视器上的锁定后(使用wait()方法),才能继续执行被唤醒的线程。
- notifyAll():唤醒在此监视器上等待的所有线程。只有当前线程放弃此同步监视器上的锁定后,才能继续执行被唤醒的线程。
下面使用银行存款取款的例子来演示用法:
public class Account {
private String accountNo;
private double balance;
// 标识账户中是否有存款的flag
private boolean flag = false;
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
@Override
public int hashCode() {
return accountNo.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Account other = (Account) obj;
if (accountNo == null) {
if (other.accountNo != null)
return false;
} else if (!accountNo.equals(other.accountNo))
return false;
return true;
}
public double getBalance() {
return balance;
}
public synchronized void withdraw(double drawAccount) {
try {
if (!flag) {
wait();
} else {
System.out.println(Thread.currentThread().getName() + "取款成功!取出:" + drawAccount + "元!");
balance -= drawAccount;
System.out.println("\t余额为:" + balance + "元!");
flag = false;
notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void deposit(double depositAccount) {
try {
if (flag) {
wait();
} else {
System.out.println(Thread.currentThread().getName() + "存款成功!存入:" + depositAccount + "元!");
balance += depositAccount;
System.out.println("\t余额为:" + balance + "元!");
flag = true;
notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
模拟取款操作的线程:
public class DrawThread extends Thread {
private Account account;
private double drawAccount;
public DrawThread(String name, Account account, double drawAccount) {
super(name); // 设置线程名
this.account = account;
this.drawAccount = drawAccount;
}
public void run() {
for (int i = 0; i < 100; i++) {
account.withdraw(drawAccount);
}
}
}
模拟存款操作的线程:
public class DepositThread extends Thread {
private Account account;
private double depositAccount;
public DepositThread(String name, Account account, double depositAccount) {
super(name);
this.account = account;
this.depositAccount = depositAccount;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
account.deposit(depositAccount);
}
}
}
测试类:
public class DrawTest {
public static void main(String[] args) {
Account account = new Account("账户", 0);
new DrawThread("取钱者", account, 888).start();
new DepositThread("存款者", account, 888).start();
}
}
运行结果:
存款者存款成功!存入:888.0元! 余额为:888.0元! 取钱者取款成功!取出:888.0元! 余额为:0.0元! 存款者存款成功!存入:888.0元! ...
可以看到存款线程与取款线程交替执行。
二、使用Lock、Condition控制线程通信
如果程序不使用synchronized关键字来保证线程同步,而是直接使用Lock对象,则系统不存在隐式的同步监视器,也就是不能使用wait()、notify()、notifyAll()方法进行线程通信了。
当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,也可以唤醒其他处于等待的线程。Lock替代同步代码块或同步方法,Condition则替代同步监视器的功能。
Condition实例被绑定在一个Lock对象上,要获得特定Lock的Condition实例,需要调用Lock对象的newCondition()方法即可。Condition类提供如下三个方法:
- await():类似于隐式同步监控器上的wait()方法,直到在其他线程调用该Condition的 signal()方法或signalAll() 方法来唤醒该线程。
- signal():唤醒在此Lock对象上等待的单个线程。如果所有线程都在Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃此同步监视器上的锁定后(使用await()方法),才能继续执行被唤醒的线程。
- signalAll():唤醒在此监视器上等待的所有线程。只有当前线程放弃此Lock对象上的锁定后,才能继续执行被唤醒的线程。
下面继续用刚刚的例子演示用法:
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;
// 标识账户中是否有存款的flag
private boolean flag = false;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
@Override
public int hashCode() {
return accountNo.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Account other = (Account) obj;
if (accountNo == null) {
if (other.accountNo != null)
return false;
} else if (!accountNo.equals(other.accountNo))
return false;
return true;
}
public double getBalance() {
return balance;
}
public void withdraw(double drawAccount) {
lock.lock();
try {
if (!flag) {
condition.await();
} else {
System.out.println(Thread.currentThread().getName() + "取款成功!取出:" + drawAccount + "元!");
balance -= drawAccount;
System.out.println("\t余额为:" + balance + "元!");
flag = false;
condition.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void deposit(double depositAccount) {
lock.lock();
try {
if (flag) {
condition.await();
} else {
System.out.println(Thread.currentThread().getName() + "存款成功!存入:" + depositAccount + "元!");
balance += depositAccount;
System.out.println("\t余额为:" + balance + "元!");
flag = true;
condition.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行结果与前面一样。
三、使用阻塞队列(BlockingQueue)控制线程
Java 5 提供了一个BlockingQueue接口(Queue的子接口),它的特征:当生产者线程试图从BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;
当消费者线程试图从BlockingQueue中取出元素时,如果该队列为空,则该线程被阻塞。
BlockingQueue提供如下两个支持阻塞的方法:
- put(E e):尝试把E元素放入BlockingQueue中,如果该队列已满,则该线程被阻塞。
- take():尝试从BlockingQueue的头部取出元素,如果该队列为空,则该线程被阻塞。
BlockingQueue包含的方法之间的对应关系:
抛出异常 | 不同返回值 | 阻塞线程 | 指定超时时长 | |
队尾插入元素 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
队头删除元素 | remove() | poll() | take() | poll(time,unit) |
获取、不删除元素 | element() | peek() | 无 | 无 |
下面用BlockingQueue实现生产者、消费者之间的线程通信
生产者线程:
import java.util.concurrent.BlockingQueue;
public class Producer extends Thread {
private BlockingQueue
bq;
public Producer(BlockingQueue
bq) {
this.bq = bq;
}
@Override
public void run() {
String[] strArr = { "qwe", "asd", "zxc" };
for (int i = 0; i < 999; i++) {
System.out.println(getName() + "生产者准备生产!");
try {
Thread.sleep(200);
bq.put(strArr[i % 3]);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "生产完成:" + bq);
}
}
}
消费者线程:
import java.util.concurrent.BlockingQueue;
public class Consumer extends Thread {
public BlockingQueue
bq;
public Consumer(BlockingQueue
bq) {
this.bq = bq;
}
@Override
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);
}
}
}
BlockingQueue测试类:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueTest {
public static void main(String[] args) {
BlockingQueue
bq = new ArrayBlockingQueue<>(1);
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
new Consumer(bq).start();
}
}
运行结果:
Thread-0生产者准备生产! Thread-2生产者准备生产! Thread-1生产者准备生产! Thread-3消费者准备消费! Thread-2生产完成:[qwe] Thread-2生产者准备生产! Thread-3消费完成:[] Thread-3消费者准备消费! Thread-1生产完成:[qwe] Thread-1生产者准备生产! Thread-3消费完成:[] ...
可以看出BlockingQueue队列中生产者每次只能放进一个元素,而且必须在消费者消费完之前阻塞生产者线程,只有消费完后才能继续下一次生产。