在解决了同步问题之后,下一步是学习任务间彼此协作。
任务协作时,关键问题是这些任务间的握手。为了实现握手,使用相同的基础特性:互斥。在互斥之上,为任务添加一种途径,可以将自身挂起,当外界条件发生变化时,在此开始执行。握手可以通过wait()、notify()或者await()、signal()来实现。
1. wait()、notify()、notifyAll()
调用sleep()、yield()时,锁并没有释放,而wait()将释放锁,这一点十分重要。它的意思是:我已经完成了我该完成的部分,接下去你们来做,需要我的时候再叫我!有两种形式调用wait(),一个有时间参数(当时间到了,或者接受到notify、notifyAll信息时恢复);无参形式,则将等待notify()、notifyAll()信息。wait()、notify()、notifyAll()是基类的一部分,并不是Thread类的一部分,因此,可以把wait()放进任何同步控制方法里,也必须放在同步方法里,否则会出现IllegalMonitorStateException。在编写程序时有个地方很重要,强烈建议用一个检查符合条件的while循环包围wait(),因为当线程需要被唤醒时,某些条件可能已经发生了改变,需要继续等待!
让我们用一个例子来演示线程通信,假设创建并启动两个任务,一个用来向账户存款,另一个从同一账户提款。当提款数额大于当前余额时,提款线程等待。不管什么时候向账户新存款,存款线程必须通知提款线程重新尝试。如果余额还是不够,则提款线程继续等待,下面是代码:
package multithreading;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadCooperationWithMonitor {
private static Account account = new Account();
public static void main(String[] args) {
System.out.println("Thread1\t\tThread2\t\t\tBalance");
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new DepositTask());
executor.execute(new Withdraw());
//不能再接受新的线程,原有线程继续执行!
executor.shutdown();
}
public static class DepositTask implements Runnable {
@Override
public void run() {
try {
while (true) {
account.deposit((int)(Math.random() * 10) + 1);
Thread.sleep(1000);
}
} catch (Exception e) {
}
}
}
public static class Withdraw implements Runnable {
@Override
public void run() {
while (true) {
account.withdraw((int)(Math.random() * 10) + 1);
}
}
}
private static class Account {
private int balance = 0;
public int getBalance () {
return balance;
}
public synchronized void withdraw (int amount) {
try {
//提款金额小于余额,等待!
while (balance < amount) {
System.out.println("\t\tWait for a deposit");
wait();
}
balance = balance - amount;
System.out.println("\t\tWithdraw " + amount + "\t\t" + getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void deposit (int amount) {
balance = balance + amount;
System.out.println("Deposit " + amount + "\t\t\t\t" + getBalance());
notifyAll();
}
}
}
代码中的49~52行尤为重要!读者请注意!
关于notify和notifyAll的思考:notify显然比notifyAll更加细致,效率更高,可以认为notify是一种优化。当等待线程只有一个的时候或者所有等待的线程被唤醒的条件都相同,则可以使用notify,否则使用notifyAll。notifyAll因某个特定锁而被调用时,只有等待这个锁的任务才会被唤醒。
下面使用显示的Lock和Condition(调用await、signalAll需要用到的类)对象重写上面的存款/ 取款业务流程:
package multithreading;
import java.util.concurrent.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadCooperation {
private static Account account = new Account();
public static void main(String[] args) {
System.out.println("Thread 1\t\tThread 2\t\tBalance");
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(new DepositTask());
executorService.execute(new WithdrawTask());
executorService.shutdown();
}
public static class DepositTask implements Runnable {
public void run() {
try {
while (true) {
account.deposit((int)(Math.random() * 10 + 1));
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static class WithdrawTask implements Runnable {
public void run() {
while (true) {
account.withdraw((int)(Math.random() * 10 + 1));
}
}
}
private static class Account {
private static Lock lock = new ReentrantLock();
private static Condition newDeposit = lock.newCondition();
private int balance = 0;
public int getBalance() {
return balance;
}
public void withdraw(int amount) {
lock.lock();
try {
while (balance < amount) {
System.out.println("\t\t\tWait for a deposit");
newDeposit.await();
}
balance -= amount;
System.out.println("\t\t\tWithdraw " + amount + "\t\t" + getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
public void deposit(int amount) {
lock.lock();
try {
balance = balance + amount;
System.out.println("Deposit " + amount + "\t\t\t\t\t" + getBalance());
newDeposit.signalAll();
}
finally {
lock.unlock();
}
}
}
}
可以看到,显示的调用Lock和Condition对象确实代码要多上许多,需要自己在finally块中释放锁。但是锁是比较灵活的!有的支持用Lock,有的支持synchronized,各有各的好,两者都会则更好!
还有许多类似的实例,大家可以自己去试试:生产者/ 消费者(缓冲区存储数据,一个读操作、一个写操作),下面的是使用显示Lock实现的一个版本!
package zy.thread.demo;
import java.util.LinkedList;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
public class ConsumerProducer {
private static Buffer buffer = new Buffer();
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new ProducerTask());
executor.execute(new ConsumerTask());
executor.shutdown();
}
private static class ProducerTask implements Runnable {
public void run() {
try {
int i = 1;
while (true) {
System.out.println("Producer writes " + i);
buffer.write(i++);
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static class ConsumerTask implements Runnable {
public void run() {
try {
while (true) {
System.out.println("\t\t\t\tConsumer reads " + buffer.read());
TimeUnit.SECONDS.sleep(3);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static class Buffer {
private static final int CAPACITY = 1;
private LinkedList<Integer> queue = new LinkedList<>();
private static Lock lock = new ReentrantLock();
private static Condition notEmpty = lock.newCondition();
private static Condition notFull = lock.newCondition();
public void write (int value) {
lock.lock();
try {
while (queue.size() == CAPACITY) {
System.out.println("\t\t\t\tWait for notFull condition");
notFull.await();
}
queue.offer(value);
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
@SuppressWarnings("finally")
public int read () {
int value = 0;
lock.lock();
try {
while (queue.size() == 0) {
System.out.println("Wait for notEmpty condition");
notEmpty.await();
}
value = queue.remove();
notFull.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
return value;
}
}
}
}
这些例子借鉴与《Java语言程序设计-进阶篇(原书第8版)》还有《Think in Java》,自己也做过修改!
下一节学习下Java封装好的一些阻塞构件