一、任务取消
1、线程状态
一个线程可以处于以下的五种状态之一:
1)新建:当线程被创建时,它只会短暂的处于这种状态。此时它已经分配了必要的系统资源,并执行了初始化。此刻线程已经有资格获取CPU时间,之后调度器将把这个线程转变为可运行状态或阻塞状态。
2)就绪:在这种状态下,调度器把时间片分给线程,线程就可以运行。
3)运行:调度器把时间片分给线程,线程开始执行任务。
4)阻塞:线程能够运行,但有某个条件阻止它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给它CPU。直到线程重新进入就绪状态。
5)死亡:处于死亡或终止的线程将不再是可调度的,并且再也不会得到CPU。线程死亡通常方式是从run()方法返回或者线程产生中断(好的线程死亡机制)。
2、线程进入阻塞状态
线程进入阻塞状态,有四个原因:
1)通过调用sleep()使任务进入休眠状态。
2)通过调用wait()使线程挂起。直到线程得到notify()或notifyAll()消息,线程才会进入就绪状态。
3)线程等待某个输入/输出的完成。
4)线程在等待其他线程释放锁。
3、中断
①、取消任务
如果外部代码能在某个操作正常完成之前将其置入“完成”状态,那么这个操作就可以称为可取消的。一个任务的取消可以有以下的原因:
1)、用户请求取消:用户点击图形界面程序中的“取消”按钮,或者通过管理接口来发出取消请求。
2)、有时间限制的操作:例如,某个应用程序需要在有限时间内搜索问题空间,并在这个时间内选择最佳的解决方案。当计时器超时时,需要取消所有正在搜索的任务。
3)、应用程序事件:应用程序对某个问题空间进行分解并搜索,从而使不同的任务可以搜索问题空间中的不同区域。当其中一个任务找到了解决方案时,所有其他仍在搜索的任务都将被取消
4)、错误:网页爬虫程序搜索相关的页面,并将页面或摘要的数据保存到磁盘。当一个爬虫任务发生错误时(比如说:磁盘已满),那么所有的任务都会去取消。
5)、关闭:当一个程序或服务关闭时必须对正在处理和等待处理的工作执行某种操作。在平缓的关闭过程中,当前正在执行的任务将继续执行直到完成,而在立即关闭的过程中,当前的任务则可能取消。
在java中没有一种安全的抢占式方法来停止线程,因此也就没有安全的抢占式方法来停止任务(尽管有Thread.stop()和suspend()方法提供终止线程的机制,他们是不安全的,可能发生死锁)。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。
使用volatile变量取消任务。下面示例中,一个线程做获取素数的任务(通过不断的轮询取消标志),在main线程中等待一秒后把取消标志置为true。
class PrimeGenerator implements Runnable {
private final List<BigInteger> primes = new ArrayList<BigInteger>();
private volatile boolean cancelled;
@Override
public void run() {
BigInteger p = BigInteger.ONE;
while(!cancelled) {
p = p.nextProbablePrime();
synchronized(this) {
primes.add(p);
}
}
}
public void canced() {
cancelled = true;
}
public synchronized List<BigInteger> get() {
return new ArrayList<BigInteger>(primes);
}
}
public class OneSecondPrimeGenerator {
public static void main(String[] args) {
PrimeGenerator generator = new PrimeGenerator();
new Thread(generator).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch(InterruptedException e) {
System.out.println("Interrupted");
} finally {
generator.canced();
}
System.out.println(generator.get());
}
}
②、中断一种好的取消任务的友好协商
线程中断是一种协作机制,线程通过这种机制来通知另外一个线程,告诉它在何时的或者可能的情况下停止当前的工作,并转而执行其它的工作。
Thread类中有三个关于中断和查询中断状态的方法分别是:
1)public void interurpt() {...} 调用该方法能中断目标线程,但不会立刻停止目标线程正在进行的工作,它只是传递中断请求,把中断标志设置为true。
2)public boolean isInterrupted() {...} isInterrupted()方法返回目标线程的中断状态
3)public static boolean interrupted() {...} 静态的interurpted()方法将清除当前线程的中断状态,并返回清除之前线程的中断状态,这是清除中断状态的唯一方法。
中断并不会真正的中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。(这些时刻也称作取消点)
*当线程在阻塞状态下遇到中断请求时:对于调用Thread.sleep(),Object.wait(),Thread.join(),或者使用BlockingQueue.put()等阻塞方法时有中断请求或者执行时发现已被设置好的中断状态,它们将清除中断状态,并抛出InterruptedException异常,表示阻塞操作由于中断而提前结束。
*当线程在非阻塞状态下遇到中断请求时:它的中断状态将被设置,然后根据将被取消的操作来检查中断以判断发生了中断。如果不触发InteruptedException异常,那么中断状态将一直保持,直到明确地清除中断状态(也就是说线程将会继续执行任务,直到有InterruptedException异常抛出,或者清除中断标志)。
在前面的素数生产者的程序中,通过设置“取消标志”进行任务的取消,下面是请求中断取消任务的使用
class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> primeQueue;
public PrimeProducer(BlockingQueue<BigInteger> primeQueue) {
this.primeQueue = primeQueue;
}
@Override
public void run() {
try {
BigInteger p = BigInteger.ONE;
while(!Thread.currentThread().isInterrupted()) {
primeQueue.put(p = p.nextProbablePrime());
}
} catch(InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " Interupted");
}
}
public void cancel() {
interrupt();
}
}
public class InterruptCancelTask {
public static void main(String[] args) throws InterruptedException {
PrimeProducer p = new PrimeProducer(new ArrayBlockingQueue<BigInteger>(10));
p.start();
TimeUnit.SECONDS.sleep(1);
p.cancel();
}
}
③、中断策略
中断策略规定线程如何解释某个中断请求——当发现中断请求时,应该做哪些工作,哪些工作单元对于中断来说是原子操作,以及以多块的速度来响应中断。
最合理的中断策略是某种形式的线程级取消操作或服务级取消操作:尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。比如说:通过线程池的shutdownNow()方法进行任务取消操作,把一个中断请求分发给多个接受者,这同时也意味着“取消当前任务”和“关闭工作者线程”。这种方式下任务就不会在某个自己拥有的线程中运行,而是在某个服务(线程池)拥有的线程中执行。这是合理的中断策略也是线程与任务对于中断响应的不同响应。
对于中断策略不应做两点假设:
1)任务不应该对执行该任务的线程的中断策略做出任何假设,除非该任务被专门设计为在服务中运行,并且在这些服务中包含特定的中断策略。
2)执行取消操作的代码也不应该对线程的中断策略做出假设。线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中,例如shutdown方法
④、更灵活的中断策略——响应中断
对于大多数可阻塞的方法中(如Thread.sleep())会抛出InterruptedException异常作为中断响应。有两种实用策略可用于处理InterruptedException:
*传递异常(可能在执行某个特定于任务的清除操作之后),使你的方法也称为可中断的阻塞方法。如下所示:
BlockingQueue<Task> taskQueue ;
public Task getNextTask() throws InterruptedException {
return taskQueue.take();
}
*恢复中断状态:通过再次调用interrupt()方法恢复中断,从而使调用栈中的上层代码能够对其进行处理。对于那些不支持取消但仍可以调用可中断阻塞方法的操作,它们必须在循环中调用这些方法,并且发现中断后重试。在这种情况下,他们应该保存本地中断状态,并且在返回前恢复中断状态而不是在捕获InterruptedException时恢复中断状态。如下所示:
public Task getNextTask(BlockingQueue<Task> queue) {
boolean interrupted = false;
try {
while(true) {
try {
return queue.take();
} catch(InterruptedException e) {
interrupted = true;
}
}
} finally {
if(interrupted)
Thread.currentThread().interrupt();
}
}
⑤、通过Future取消任务
ExecutorService.submit将返回一个Future来描述任务。Future拥有一个cancel()方法,该方法带有一个boolean类型的参数mayInterrupteIfRunning,表示取消操作是否成功(这只是表示任务是否能够接收中断,而不是表示任务是否能检测并处理中断)
如果mayInterruptIfRunning为true并且任务正在某个线程中运行,那么这个线程能够被中断。如果mayInterruptIfRunning为false,那么意味着“任务没有启动,就不要运行它”,这种方式应该用于那些不处理中断的任务中。
每个线程都有自己的中断策略,随意使用Future.cancel()方法是不提倡的,因为你不知道中断发生时线程会发生什么。最好是通过Executor框架创建执行任务的线程,它实现了一种中断策略使得任务可以通过中断被取消。此外,在尝试取消某个任务时,不宜直接中断线程池(调用ExecutorService.shutdown()等方法),因为你不知道中断请求到达时正在运行什么任务,最好通过任务的Future.cancel()取消任务。
public class FutureInteruptTask {
static final ExecutorService taskExec = Executors.newCachedThreadPool();
public static void timedRun(Runnable r, long timeout, TimeUnit unit) throws InterruptedException {
Future<?> task = taskExec.submit(r);
try {
task.get(timeout, unit);
} catch (ExecutionException e) {
// 接下任务将被取消
} catch (TimeoutException e) {
// 任务如果抛出异常,那么重新抛出异常
} finally {
// 任务结束,取消操作不会造成什么影响
// 任务未结束,那么直接中断线程
task.cancel(true);
}
}
}
5、处理不可中断的阻塞
如果一个线程由于执行同步的Socket I/O或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有其他任何作用。不可中断的阻塞总共有以下几种形式以及解决办法:
1)Java.io包中的同步Socket I/O:在服务器应用程序中,最常见的阻塞I/O形式就是对套接字进行读取和写入。虽然InputStream和OutputStream中的read和write等方法都不会响应中断,但通过关闭底层套接字,可以使得执行read或write等方法而被阻塞的线程抛出一个SocketExceptione
2)Java.io包中的同步I/O:当中断一个正在InteruptibleChannel上等待的线程时,将抛出ClosedByInterruptException并关闭链路(这还会使得其他在这条链路上阻塞的线程同样抛出ClosedByInterruptException)。当关闭一个InteruptibleChannel时,将导致所有在链路操作上阻塞的线程都抛出AsynchronousCloseException。
3)Selector的异步I/O:如果一个线程在调用Selector.select方法(在java.nio.channels中)时阻塞了。那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回。
4)获取某个锁:如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求。在Lock类中提供LockInteruptibly()方法,用于可响应中断等待一把锁。
①、通过改写interrupt()方法解决I/O阻塞问题。
下面程序中改写了interrupt()方法,使其既能处理标准的中断,也能关闭底层的套接字。因此无论线程在read()或write()方法中阻塞还是某个可中断的阻塞方法中阻塞,都可以被中断并停止执行当前的工作。
public class ReaderThread extends Thread {
private final Socket socket;
private final InputStream in;
private static final int SIZE = 1024;
public ReaderThread(Socket socket) throws IOException {
this.socket = socket;
this.in = socket.getInputStream();
}
public void interupt() {
try {
socket.close();
} catch(IOException e) {
e.printStackTrace();
} finally {
super.interrupt();
}
}
protected void processBuffer(byte[] buffer, int count) {
// 保存数据
}
@Override
public void run() {
try {
byte[] buffer = new byte[SIZE];
while(true) {
int count = in.read(buffer);
if(count < 0)
break;
else if(count > 0)
processBuffer(buffer, count);
}
} catch(IOException e) {
// 线程退出 (当套接字关闭时,抛出SocketException异常(继承自IOException))
}
}
}
6、ExecuotrService
①、线程池的生命周期
Executor框架作为线程池的实现,它提供了灵活且强大的异步任务执行基础,支持多种不同类型的任务执行策略,并提供了一种标准的方法将任务的提交过程与执行过程进行解耦,并用Runnable来表示任务。ExecutorService接口扩展了Executor接口,添加了用于生命周期管理的方法。
ExecutorService中生命周期管理方法:
public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit) thorows InterruptedException;
}
ExecutorService管理的线程池的生命周期有3中状态:运行、关闭、终止。
1)运行:ExecutorService在初始创建时处理运行状态,此时工作队列中任务可能处于完成、正在运行、等待工作线程执行任务。
2)关闭:ExecutorService的关闭可以由shutdown()或shutdownNow()方法进行关闭,前者将执行平缓的关闭,后者是强制性关闭。可以通过调用isShutdown()来轮询ExecutorService是否已经关闭。
3)终止:当ExecutorService关闭后提交的任务将由“拒绝执行处理器”进行处理,它会抛弃任务,或者使得executor方法抛出一个未受检查的RejectedExecutionException。当所有任务完成后,ExecutorService将转入终止状态。可以通过调用awaitTermination()来等待ExecutorService到达终止状态,或者通过调用isTerminated来轮询ExecutorService是否已经终止。
②、再谈ExecutorService的关闭
关闭ExecutorService(生产者——消费者服务模式)有两种方式,调用shutdown()或shutdownNow()。
1)调用shutdown():使用shutdown()是平缓的关闭,它更安全,因为ExectorService会一直等到队列中的所有任务都执行完后才关闭。但它的关闭速度慢,响应性差。
2)调用shutdownNow():使用shutdownNow()是强制关闭(通过向所有线程池中的线程发送interrupt()中断请求),shutdownNow首先关闭当前正在执行的任务,然后返回所有尚未启动的任务清单(List<Ruunable>对象)。强制关闭速度快,但风险大,因为任务可能在执行一半的时候被结束。
通过封装,把关闭线程池的方法放在更高级别的服务中。
public void stop() throws InterruptedException {
try {
exec.shutdown();
exec.awaitTermination(TIMEOUT, TimeUnit.MILLISECONDS);
} finally {
writer.colse();
}
}
二、线程间的通信
1、任务间的协作
在解决线程安全性我们使用了同步机制限制多个线程对于共享资源的访问,但在另外方面我们希望多个线程可以进行协作以解决某个特定的问题。这些问题解决必须要依赖于某一部分的完成,才能继续下一部分的工作。这就好比于项目的规划:必须先挖房子的地基,然后才能进行混凝土浇注,而管道的铺垫必须要水泥板浇注之前完成,等等。这样,某些任务的执行必须要某一项相关任务完成之前才能开始。
解决任务协作关键是任务间的“握手”,完成任务“握手”必须依赖于同步机制。这样,才能确保只有一个任务可以响应某个信号,就能根除任何可能的竞争条件。“握手”可以通过Object的wait()和notify()方法来安全地实现。
2、条件队列
条件队列使得一组线程(称之为等待线程集合)能够通过某种方法来等待特定的条件变成真。
每个Java对象都可作为一把锁,每个对象同样可以作为一个条件队列(比如说这个对象是一个任务对象),Object中wait()、notify()、notifyAll()方法就构成内部条件队列API。对象的内置锁与其内部条件队列是相互关联的,即要调用内部条件队列的任何一个方法,必须持有对象的锁。这是由于“等待由状态构成的条件”与“维护状态一致性”这两种机制必须紧密地捆绑在一起:“只有能对状态进行检查时,才能在某个条件上等待。只有能修改状态时,才能使条件等待中释放另一个线程。”
①、状态依赖性
状态依赖性使得某些操作有着基于状态的前提条件。比如:不能从一个空队列中删除元素,或者不能获取一个尚未结束的任务的计算结果。这些操作可以执行前,必须等待队列进入“非空”,或者任务进入“已完成”的状态。
*在单线程中:调用一个方法时,如果某个基于状态的前提条件未得到满足(例如“连接池必须非空”),那么这个条件将永远无法成真。因此,在编写单线程方法时要使得这些类在它们的前提条件未被满足前就失败。
*在多线程中:基于状态的条件可能会由于其他线程的操作而改变。编写多线程依赖状态方法时,最好的选择是,即等待“前提条件”变为真。
多线程条件下,构建有界缓存队列,实现状态依赖性。
// 有界缓存
public abstract class BaseBoundedBuffer<V> {
private final V[] buf;
// 指向缓存数组的数据末尾(放数据)
private int tail;
// 指向缓存数组的数组头部(取数据)
private int head;
// 记录缓存数组中数据的个数
private int count;
protected BaseBoundedBuffer(int capacity) {
this.buf = (V[]) new Object[capacity];
}
protected synchronized final void doPut(V v) {
buf[tail] = v;
if(++tail == buf.length)
tail = 0;
++count;
}
protected synchronized final V doTake() {
V v = buf[head];
// 取出数据后把当前位置置为null
buf[head] = null;
if(++head == buf.length)
head = 0;
--count;
return v;
}
public synchronized final boolean isFull() {
return count == buf.length;
}
public synchronized final boolean isEmpty() {
return count == 0;
}
}
// 状态依赖性
public class GrumpyBoundedBuffer<V> extends BaseBoundedBuffer<V> {
protected GrumpyBoundedBuffer(int capacity) {
super(capacity);
}
public synchronized void put(V v) throws BufferFullException {
if(isFull())
throw new BufferFullException();
doPut(v);
}
public synchronized V take() throws BufferEmptyException {
if(isEmpty())
throw new BufferEmptyException();
return doTake();
}
}
②、条件谓词
条件谓词是使某个操作称为状态依赖操作的前提条件,如果没条件谓词,条件等待机制就无法完成(多线程下状态依赖性无法成立)。在条件缓存中,只有当缓存不为空时,take方法才能执行,否则必须等待。对于take方法来说,它的条件谓词就是“缓存不为空”。同样,put方法的条件谓词是“缓存不满”。
在条件等待中存在一种重要的三元关系,包括加锁、wait方法和一个条件谓词。在条件谓词中包含多个状态变量,而状态变量由一个锁来保护,因此在测试条件谓词前必须先持有这个锁。锁对象与条件队列对象必须是同一个对象。
③、wait()
由于线程间存在状态依赖性(一个线程任务依赖于另外一个线程任务)。便有了条件队列,条件队列使得线程必须通过条件等待来实现线程间的协作。所以线程协作的核心概念是条件等待,它的前提是获取对象锁,核心是条件谓词,实现是wait()。
使用wait的形式如下:
syncrhonized(lock) { // 前提,获取对象锁
while(!conditionPredicate()) // 核心,条件谓词
lock.wait(); // 实现,等待。
}
使用wait()方法时将发生以下几件事:
1)释放当前线程获取的对象锁。
2)线程进入条件队列,进入阻塞状态。
3)等待另一个线程修改状态,使条件谓词为真,并调用notify()唤醒它。当唤醒时它会自动的获取对象锁并从wait()调用中返回。
每次wait()调用都会隐式地与特定的条件谓词关联起来。当调用某个特定条件谓词的wait时,调用者必须已经持有与条件队列相关的锁,并且这个锁必须保护构成条件谓词的状态变量。
—— 摘自《java并发编程实战》
④、notify()与notifyAll()
我们可以通过通知方式唤醒在条件队列等待的线程。通知有两种方式notify()以notifyAll()方法。
1)notify():在调用notify时,JVM会从这个条件队列上等待的多个线程中选择一个进行唤醒。
2)notifyAll():而使用notifyAll时,JVM则直接唤醒所有在条件队列上等待的线程。
使用通知应注意下面两个问题:
1)在使用notify与notifyAll时必须持有条件队列对象的锁,这样才能唤醒在条件队列上等待的线程。
2)调用完通知方法后,应快速释放条件队列对象锁,因为唤醒的线程不能重新获得锁,那么将无法从wait中返回。
每当在等待一个条件时,一定要确保在条件谓词变为真时,通过某种方式发出通知。
使用条件队列实现有界缓存
public class BoundedBuffer<V> extends BaseBoundedBuffer<V> {
public BoundedBuffer(int capacity) {
super(capacity);
}
public synchronized void put(V v) throws InterruptedException {
// isFull()--条件谓词
while(isFull())
// 阻塞并直到:非满
wait();
doPut(v);
notifyAll();
}
public synchronized V take() throws InterruptedException {
// isEmpty()--条件谓词
while(isEmpty())
// 阻塞并直到:非空
wait();
V v = doTake();
notifyAll();
return v;
}
}
⑤、丢失的信号
当线程必须等待一个已经为真的条件,但在开始等待之前没有检查条件谓词。此时,线程将等待一个已经为真的事件。这就好比,设置闹钟,在某个时候做某件事,但因为一些原因没有听到这个铃,使得我们一直在苦苦等待铃声的到来。
*notify的危险性与notifyAll的性能:
当多个线程基于不同的条件谓词在同一个条件队列上等待,如果使用notify而不是notifyAll,那么将是一种危险的操作,因为单一的通知很容易造成类似信号丢失的问题。比如说:有三个线程,线程A在条件谓词PA等待,线程B在条件谓词PB等待。因为线程C修改了状态,使条件谓词PB称为真,通过调用notify想唤醒线程B,但JVM通过选择线程A进行唤醒,但PA条件谓词为假,所以线程A继续进入等待。此时,线程B本可以继续执行,但因为丢失的信号却没有被唤醒。所以使用notify应遵循两个条件:
1)所有等待线程的类型都相同(执行同一个任务):只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后将执行相同的操作。
2)单进单出:在条件变量上的每次通知,最多只能唤醒一个线程来执行。
所以,最普遍的做法是优先使用notifyAll,它的性能比notify低效,但更容易保证正确性。在使用notifyAll时,将唤醒所以在条件队列等待的线程,并使它们在锁上进行竞争。最终,大多数将又回到休眠状态。因而,将会出现大量的上下文切换操作以及竞争的锁获取操作(最坏情况下,将导致进行O(n^2)唤醒操作)。是考虑安全性还是性能,应该由具体情况具体分析。
3、生产者——消费者模式
我们可以使用条件队列构建安全的生产者——消费者模式。下面是使用这种模式的餐馆模型。其中厨师代表生产者,生产食物。服务员代表消费者。两个任务必须在生产和消费时进行状态依赖。生产者等待食物是否被消费(条件谓词)。消费者必须等待食物是否被生产(条件谓词)。以此产生协作。
class Meal {
private final int orderNum;
public Meal(int orderNum) {
this.orderNum = orderNum;
}
public String toString() {
return "Meal " + orderNum;
}
}
// 服务员(消费者)任务
class WaitPerson implements Runnable {
private Restaurant restaurant;
public WaitPerson(Restaurant restaurant) {
this.restaurant = restaurant;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
synchronized(this) {
// 条件谓词
while(restaurant.meal == null) // 采用while()条件有两个原因:
// 其一:避免唤醒信号丢失,导致死锁,
// 其二,避免在信号丢失之后,多个任务将会锁在此处
// 释放waitPerson对象锁,等待waitPerson对象调用notify()方法停止其等待
wait(); // wait()/notify()方法必须在同步代码块或方法中使用
}
System.out.println("Waitperson got " + restaurant.meal + Thread.currentThread().getName());
synchronized(restaurant.chef) {
restaurant.meal = null;
// 在生产者等待的条件队列对象调用notifyAll()通知生产者
restaurant.chef.notifyAll();
}
}
} catch(InterruptedException e) {
System.out.println("WaitPerson interrupted");
}
}
}
// 厨师(生产者)任务
class Chef implements Runnable {
private Restaurant restaurant;
private int count = 0;
public Chef(Restaurant restaurant) {
this.restaurant = restaurant;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
synchronized(this) {
// 当厨师已经生产事务后,条件谓词满足。
while(restaurant.meal != null)
// 等待食物被消费后消费者通知生产者
wait();
}
if(++count == 10) {
System.out.println("Out of food closing");
restaurant.exec.shutdownNow();
}
System.out.println("Order up!");
// 由于服务员任务被wait(),所以在厨师任务此处,将会获取waitPerson对象锁
synchronized(restaurant.waitPerson) {
restaurant.meal = new Meal(count);
// 唤醒等待waitPerson锁的任务(在本程序,其实调用notify()即可,但处于安全性的考虑所以调用notifyAll()更安全点)
// 调用消费者条件队列对象的notifyAll()唤醒消费者
restaurant.waitPerson.notifyAll();
}
TimeUnit.MILLISECONDS.sleep(100);
}
} catch(InterruptedException e) {
System.out.println("Chef interrupted");
}
}
}
public class Restaurant {
Meal meal;
ExecutorService exec = Executors.newCachedThreadPool();
WaitPerson waitPerson = new WaitPerson(this);
WaitPerson waitPerson2 = new WaitPerson(this);
Chef chef = new Chef(this);
public Restaurant() {
exec.execute(chef);
exec.execute(waitPerson);
exec.execute(waitPerson2);
}
public static void main(String[] args) {
new Restaurant();
}
}
4、显示条件队列——Condition对象
每个内置锁只能有一个相关联的条件队列,多个线程可能在同一个条件队列上等待不同的条件谓词,并且在最常见的加锁模式下公开条件队列对象。这些因素都使得无法满足在使用notifyAll时所有等待线程为同一类型的需求。
我们可以使用Lock和Condition对象构建显示锁与显示条件队列以满足对于不同条件下的需求(比如满足通知同一类型的线程)。一个Lock可以与多个Condition关联一起,而一个Condition只能与一个Lock关联。可以通过Lock.newCondition创建Condition对象。在每个锁上可以存在多个等待、条件等待可以是可中断或不可中断的、基于时限的等待、以及公平或非公平的队列操作。Conditon对象还继承了Lock对象的公平性,对于公平的Lock,线程将按照FIFO的顺序依次从Condition.await()释放锁。
在Condition对象中,与wait、notify、notifyAll方法对应的是await、signal、signalAll。
使用显示锁与显示条件队列构建有界缓存,这个程序与使用内置锁的条件队列构建的有界缓存行为是一致的。不同的在于,可以使用两个条件队列构建两个条件谓词并分发到两个等待的线程中,这更易于管理。
public class ConditionBoundedBuffer<V> {
protected final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private static final int SIZE = 1024;
private final V[] items = (V[]) new Object[SIZE];
private int tail;
private int head;
private int count;
public void put(V v) throws InterruptedException {
lock.lock();
try {
// 条件谓词
while(count == items.length)
// 阻塞并直到:非满
notFull.await();
items[tail] = v;
if(++tail == items.length)
tail = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public V take() throws InterruptedException {
lock.lock();
try {
// 条件谓词
while(count == 0)
// 阻塞并直到:非空
notEmpty.await();
V v = items[head];
items[head] = null;
if(++head == items.length)
head = 0;
--count;
notFull.signal();
return v;
} finally {
lock.unlock();
}
}
}
5、阻塞队列
阻塞队列提供了可阻塞的put和take方法,调用这两个方法将会抛出受检查异常InterruptedException。以及支持定时的offer和poll方法。如果队列已经满了,那么put方法将会阻塞知道有空间可用;如果队列为空,那么take方法将会阻塞直到有元素可用。阻塞队列通过条件队列实现,其中使用了显示的Lock与Condition对象构建条件等待与唤醒。
阻塞队列分为下列四种:
ArrayBlockingQueue | 基于数组实现,它具有固定尺寸的数组,用于缓存数据。它是一个FIFO队列,与ArrayList类似,但比它同步的List具有更好的并发性。最常用的阻塞队列 |
LinkedBlockingQueue | 基于链表实现,是一个无界的缓存队列。它也是一个FIFO队列,与LinkedList类似。 |
PriorityBlockingQueue | PriorityBlockingQueue是一个按优先级排序的队列,不同于FIFO它的排序按照你希望的排序算法进行,其中元素需要实现Comparable接口。 |
SynchronousQueue | SynchronousQueue实际上并不是一个正在的队列,因为它不会为队列中的元素维护存储空间。它维护了一组线程,这些线程在等待这把元素加入或移出队列。 |
使用阻塞队列完成线程协作最常见的应用是通过生产者——消费者模式进行实现。在基于阻塞队列构建的生产者——消费者设计中,当数据生成时,生产者把数据放入队列,而当消费者准备处理数据时,将从队列中获取数据。生产者不需要知道消费者的标识或数量,或者它们是否是唯一的生产者,而只需将数据放入队列即可。同样,消费者也不要知道生产者是谁,或者工作来自何处。
三、死锁
1、哲学家就餐问题
由Edsger Dijkstra提出的哲学家就餐问题是一个经典的死锁例证。问题描述了“”5个哲学家去就餐,坐在一张圆桌旁。他们又5根筷子,并且每两个人中间放一根筷子。哲学家们时而思考,时而就餐。每个人都需要一双筷子才能吃到东西,并且吃完后将筷子放回原处继续进行思考。每个哲学家在就餐时都会抓住自己左边的筷子,然后等待右边的筷子空出来,但同时又不放下已经拿到的筷子。在这种情况下每个哲学家都可能“饿死”。
这个问题说明:每个人都拥有其他人需要的资源,同时又等待其他人已经拥有的资源,并且每个人在获得锁需要的资源之前都不会放弃以有的资源。