1、取消
(1)生产者消费者问题中,如果采用BlockingQueue阻塞队列。假如生产者生产的速度超过了消费者的处理速度,队列将被填满,put操作也会被阻塞。当生产者在put方法中阻塞时,如果消费者希望取消生产者任务,它可以通过调用cancel方法来设置cancelled标志,但此时生产者却永远不能检查这个标志,因为它无法从阻塞的方法中恢复过来(消费者已经停止从队列中取出素数,所以put方法将一直保持阻塞状态)。此时任务永远不会结束。
如下面代码:
public class BrokenPrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
private volatile boolean cancelled = false;
static int produceCount = 1;
static int consumeCount = 1;
BrokenPrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
public void run() {
try {
BigInteger p = BigInteger.ONE;
synchronized (BrokenPrimeProducer.class) {
while (!cancelled) {
queue.put(p = p.nextProbablePrime());// 生产者生成素数放入队列
System.out.println("===生产===" + produceCount++);
}
}
System.out.println("!!结束!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void cancel() {
cancelled = true;
System.out.println("!!请停止生产吧");
}
static void consumePrimes() throws InterruptedException {
BlockingQueue<BigInteger> primes = new LinkedBlockingQueue<>(10);
BrokenPrimeProducer producer = null;
for (int i = 0; i < 100000; i++) {
producer = new BrokenPrimeProducer(primes);
producer.start();
}
try {
while (needMorePrimes()) {// 仍需要素数时,从中取出
consume(primes.take());
}
} finally {
producer.cancel();// 取消生产
}
}
private static void consume(BigInteger take) {
System.out.println("==取出第==" + consumeCount++ + "个素数:" + take);
}
private static boolean needMorePrimes() {
if (consumeCount < 11) {
return true;
}
return false;
}
public static void main(String[] args) throws InterruptedException {
consumePrimes();
}
}
运行结果:
(2)中断取消
在上面示例中,幸运的话有可能会取消,但多数情况是没有办法检测到cancelled标志,一直阻塞。可以采用中断的方法取消。
Thread的中断方法有:
public class Thread{
public void interrupt(){}//中断线程
public static boolean interrupted(){}//清除中断状态
public boolean isInterrupted(){}
}
中断并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。
将示例代码做出改动:
public void run() {
try {
BigInteger p = BigInteger.ONE;
synchronized (BrokenPrimeProducer.class) {
while (!Thread.currentThread().isInterrupted()) {
queue.put(p = p.nextProbablePrime());// 生产者生成素数放入队列
System.out.println("===生产===" + produceCount++);
}
}
} catch (InterruptedException e) {
System.out.println("!!结束!!");
e.printStackTrace();
}
}
public void cancel() {
interrupt();
}
static void consumePrimes() throws InterruptedException {
BlockingQueue<BigInteger> primes = new LinkedBlockingQueue<>(10);
BrokenPrimeProducer producer = null;
List<BrokenPrimeProducer> producers = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
producer = new BrokenPrimeProducer(primes);
producer.start();
producers.add(producer);
}
try {
while (needMorePrimes()) {// 仍需要素数时,从中取出
consume(primes.take());
}
} finally {
for (BrokenPrimeProducer p : producers) {// 如果有多个生产者,需要一个个中断
p.cancel();// 取消生产
}
}
}
结果:
在每次迭代循环中有两个地方可以检测出中断,一个是put方法调用,一个是循环开始处查询状态时。
(3)如果除了将InterruptedException传递给调用者外还要进行额外的操作,则需要在捕获InterruptedException之后恢复中断状态:Thread.currentThread().interrupt()。
在取消过程中可能涉及除了中断状态之外的其他状态,,中断可以用来获得线程的注意,并且由中断线程保存的信息,可以为中断的线程提供进一步的指示。(当访问这些信息时,要保证使用同步)
例如,当一个ThreadPoolExecutor拥有的工作者线程检测到中断时,它会检查线程池是否正在关闭。如果是,它会在结束之前执行一些线程池清理工作,否则它可能创建一个新线程将线程池恢复到合理的规模。
(4)计时运行
这是一种简单的方法,但却破坏了一下规则:在中断线程之前,应该了解它的中断策略。由于timedRun可以从任意一个线程调用,因此无法知道这个调用线程的中断策略。我的理解是,因为这个方法不知道调用的方法后续的处理,所以如果捕获异常,应该抛回去给调用方处理。改进如下:
(5)
(6)可以根据自己的需求来重写中断方法
2、关闭
(1)中断。当取消一个生产者-消费者操作时需要同时取消生产者和消费者。
(2)设置“请求关闭”标志
(3)使用ExecutorService
(4)“毒丸”对象。“Poison Pill”是指一个放在队列上的对象,当得到这个对象时,立即停止。在FIFO队列中,“Poison Pill”将确保消费者在关闭之前首先完成队列中的所有工作,在提交“毒丸”对象之前提交的所有工作都会被处理,而生产者在提交了“毒丸”对象后,将不会再提交任何工作。
注:只有生产者和消费者数量已知的情况下才可以使用毒丸对象。