并发编程实战-取消与关闭


本章介绍了,如何在线程执行完正常工作之前,提前结束
java没有提供任何机制安全的终止线程.但它提供了中断,这是一种协作机制,能使一个线程终止另一个线程的当前工作.

1.任务取消

如果外部代码能够在某个操作完成之前将其置入"完成"状态,那么这个操作就称为可取消的,取消操作的原因很多:

  • 用户请求取消
  • 有时间限制的操作
  • 应用程序事件:多个任务执行搜索,一旦某个任务搜索完成,取消其它任务
  • 错误
  • 关闭:程序或服务关闭

一种协作机制是设置某个"已取消请求"的标志,任务将定期查看该标志,如果为true,那么任务将提前结束

@ThreadSafe
public class PrimeGenerator implements Runnable{
    @GuardedBy("this")
    private final List<BigInteger> primes = new ArrayList<>();
    //使用volatile类型的域来保存这个状态
    private volatile boolean cancelled;
    @Override
    public void run() {
        BigInteger p = BigInteger.ONE;
        while (!cancelled) {
            p = p.nextProbablePrime();
            synchronized (this) {
                primes.add(p);
            }
        }
    }
    public void cancel() {
        cancelled = true;
    }
    public synchronized List<BigInteger> get() {
        return new ArrayList<>(primes);
    }
}

下面是调用方法:

List<BigInteger> aSecondOfPrimes() throws InterruptedException {
        PrimeGenerator primeGenerator = new PrimeGenerator();
        new Thread(primeGenerator).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } finally {
            primeGenerator.cancel();
        }
        return primeGenerator.get();
    }

1.1.中断

上面的例子可能产生一个严重的问题,例如调用一个阻塞方法

public class BrokenPrimeProducer extends Thread{
    private final BlockingQueue<BigInteger> queue; 
    private volatile boolean cancelled;
    public BrokenPrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        BigInteger p = BigInteger.ONE;
        while (!cancelled) {
            try {
            	//放到队列中,这一步可能发生阻塞
                queue.put(p = p.nextProbablePrime());
            } catch (InterruptedException e) {
            }
        }
    }
    public void cancel() {
        cancelled = true;
    }
    void consumePrimes() throws InterruptedException {
        BlockingQueue<BigInteger> primes = new ArrayBlockingQueue<>(18);
        BrokenPrimeProducer producer = new BrokenPrimeProducer(primes);
        producer.start();
        try {
            while (needMorePrimes()) {
                consume(primes.take())
            }
        } finally {
            producer.cancel();
        }
    }
}

如果生产者速度大于消费者速度,队列将被填满,put方法将被阻塞,如果这时消费者希望取消生产者任务,此时生产者永远无法识别cancelled状态,因为它无法从阻塞状态恢复过来(因为此时消费者已经停止从队列中取数,所以put方法将一直阻塞)

Thread类提供了一些用于查询线程中断状态和中断线程的方法
Thread.isInterrupted方法能返回目标线程的中断状态,静态的interrupted方法将清除当前线程的中断状态,并返回它之前的值.

阻塞库方法,例如Thread.sleep()和Object.wait等,都会检查线程何时中断

当线程在非阻塞状态下中断时,它的中断状态将被设置,然后根据将被取消的操作来检查中断状态以判断发生了中断,通过这种方法,中断操作将变得有"黏性"–如果不触发InterruptedException,那么中断状态将被一直保持,直到明确的清除中断状态

调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息.

中断操作不会真正的中断一个正在运行的线程,而是发出中断请求,然后由线程在下一个合适的时刻中断自己

通常,中断是实现取消的最合理的方式

class PrimeProducer extends Thread {
    private final BlockingQueue<BigInteger> queue;
    PrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        BigInteger p = BigInteger.ONE;
        while (!Thread.currentThread().isInterrupted()) {
            try {
                queue.put(p = p.nextProbablePrime());
            } catch (InterruptedException e) {
                //允许线程退出
            }
        }
    }
}

1.2 中断策略

线程应该包含中断策略,规定线程如何解释某个中断请求–当发现中断请求时,应该做哪些工作,哪些工作单元对中断来说是原子操作,以及以多块的速度来响应中断.

最合理的中断策略是某种形式的线程级取消操作或服务级取消操作:尽快退出,必要时进行清理,通知某个所有者线程已经退出.
还可以建立其它的中断策略,例如暂停服务或重新开始服务.

区分任务和线程对中断的反应是很重要的
当检查到中断请求时,任务并不需要放弃所有操作–它可以推迟处理中断请求,并直到某个更合适的时刻.

由于每个线程拥有各自的中断策略,因此除非你直到中断对该线程的含义,否则就不应该中断这个线程

1.3 响应中断

当调用可终端的阻塞函数时,如Thread.sleep()或BlockingQueue.put等,由两种实用策略可用于处理InterruptedException:

  • 传递异常(throws 异常),从而使你的方法也成为可中断的阻塞方法.
  • 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理

如果不想传递异常,那么可以通过再次调用interrupt方法来恢复中断状态.

只有实现了线程中断策略的代码才可以屏蔽中断请求.在常规的任务和库代码中都不应该屏蔽中断请求

可以使用Thread的join方法,将任务通过Thread.join(long millis)执行.
如果线程被其它线程中断,join方法会抛出异常,供调用者处理


    /**
     * Waits at most {@code millis} milliseconds for this thread to
     * die. A timeout of {@code 0} means to wait forever.
     *
     * <p> This implementation uses a loop of {@code this.wait} calls
     * conditioned on {@code this.isAlive}. As a thread terminates the
     * {@code this.notifyAll} method is invoked. It is recommended that
     * applications not use {@code wait}, {@code notify}, or
     * {@code notifyAll} on {@code Thread} instances.
     *
     * @param  millis
     *         the time to wait in milliseconds
     *
     * @throws  IllegalArgumentException
     *          if the value of {@code millis} is negative
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */
    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

join的不足:无法知道执行控制是因为线程正常退出而返回还是因为join超时而返回.

1.4 通过Future实现取消

    private static final ExecutorService executor = Executors.newFixedThreadPool(100);
    public static void timedRun( Runnable r,long timeout,TimeUnit unit) throws InterruptedException {
        Future<?> task = executor.submit(r);
        try {
            task.get(timeout,unit);
        } catch (ExecutionException e) {
            //如果在任务中抛出了异常,那么重新抛出该异常
            throw e;
        } catch (TimeoutException e) {
            //如果超时,接下来的任务将被取消,这里简化到全部在finally中执行
        } finally {
        	//如果任务已经结束,那么执行取消操作也不会有任何影响
        	//如果任务正在运行,那么将被中断
            task.cancel(true);
        }

当Future.get抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.calcel来取消任务.

1.5 处理不可中断的阻塞

很多阻断方法都是通过提前响应或是抛出InterruptedException来响应中断请求,但并非所有的可阻塞方法或者阻塞机制都能响应中断
如果一个线程由于执行同步外的Socket I/O 或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有任何其他作用.
可以使用类似于中断的手段来停止这些线程,但要求必须知道线程阻塞的原因.

  • Java.io包中的同步Socket I/O.
  • Java.io包中的同步 I/O.
  • Selector的异步I/O.
  • 获取某个锁:如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁所以不会理会中断请求.但在Lock类中提供了lockInterruptibly方法,该方法在等待一个锁的同时仍能响应中断.
public class ReaderThread extends Thread{
    private final Socket socket;
    private final InputStream in;
    public ReaderThread(Socket socket) throws IOException {
        this.socket = socket;
        this.in = socket.getInputStream();
    }
    /**
     * 重写了interrupt,使其既能处理标准的中断,又能关闭底层套接字
     */
    @Override
    public void interrupt() {
        try {
            socket.close();
        } catch (IOException e) {} 
        finally {
            super.interrupt();
        }
    }
    @Override
    public void run() {
        try {
            byte[] buf = new byte[30];
            while (true) {
                int count = in.read(buf);
                if (count < 0) {
                    break;
                } else if (count > 0) {
                    //do something
                }
            }
        } catch (IOException e) {
            //允许线程退出
        }
    }
}

2.停止基于线程的服务

线程的所有权是不可逆转的:应用程序拥有服务,服务拥有工作线程,但应用程序并不能拥有工作线程,因此应用程序不能直接停止工作线程.

服务应该提供生命周期方法来关闭它自己以及它拥有的线程

对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法

2.1 示例:日志服务

日志服务应该用一条单独的线程,可调用日志服务,将日志提交给BlockingQueue,通过BlockingQueue将消息提交给日志线程,并由日志线程写入.
但是当队列满时,生产者线程会阻塞,这些阻塞的线程无法解除阻塞状态.当取消一个生产者-消费者操作时,需要同时取消生产者和消费者.
关闭LogWriter的方法是,设置某个"已请求关闭"标志,以避免进一步提交日志
为了避免竞态条件问题,可以使日志消息的提交操作成为原子操作.:通过原子方式来检查关闭请求,并且有条件的递增一个计数器来"保持"提交消息的权利.

public class LogService {
    private final BlockingQueue<String> queue;
    private final LoggerThread loggerThread;
    private final PrintWriter writer;
    //关闭标志
    @GuardedBy("this")
    private boolean isShutdown;
    //防止竞态条件,设置一个计数器
    @GuardedBy("this")
    private int reservations;
    public LogService(Writer writer) {
        this.queue = new LinkedBlockingQueue<>(30);
        this.loggerThread = new LoggerThread();
        this.writer = new PrintWriter(writer);
    }
    public void start() {
        loggerThread.start();
    }
    public void stop() {
        synchronized (this) { isShutdown = true;}
        loggerThread.interrupt();
    }
    public void log(String msg) throws InterruptedException {
        synchronized (this) {
            if (isShutdown) {
                throw new IllegalStateException();
            }
            ++reservations;
        }
        queue.put(msg);
    }
    private class LoggerThread extends Thread {
        @Override
        public void run() {
            try {
                while (true) {
                    try {
                        synchronized (LogService.this) {
                            if (isShutdown && reservations == 0) {
                                break;
                            }
                        }
                        String msg = queue.take();
                        synchronized (LogService.this) {
                            --reservations;
                        }
                        writer.println(msg);
                    } catch (InterruptedException e) {
                        /* retry */
                    }
                }
            } finally {
                writer.close();
            }
        }
    }
}

ExecutorService提供了shutdown和shutdownNow方法,一般推荐使用shutdown,避免风险
通过关闭公共的ExecutorService,也可以达到中断线程的目的

public class LogService {
    private final ExecutorService exec = Executors.newSingleThreadExecutor();
    private final PrintWriter writer;
    public LogService(PrintWriter writer) {
        this.writer = writer;
    }
    public void stop() {
        try {
            exec.shutdown();
            exec.awaitTermination(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {

        }finally {
            writer.close();
        }
    }
}

2.2 "毒丸"对象

含义:当得到这个对象时,立即停止.
即FIFO队列中,"毒丸"对象将确保消费者在关闭之前首先完成队列中的所有工作,在提交"毒丸"之前提交的所有工作都会背处理,而生产者提交了"毒丸"对象后,将不会再提交任何工作.

只有当生产者和消费者数量都已知的情况下,才可以使用"毒丸"对象
多生产者时,只需每个生产者都像队列中添加一个"毒丸",消费者在接收到指定数量"毒丸"后停止消费即可.反之亦然
当生产者-消费者数量过大时,这种方法将难以适用

2.3 示例:只执行一次的服务

如果某个方法需要处理一批任务,并且当所有任务都处理完成后才返回,那么可以通过一个私有的Executor来简化服务的生命周期管理.

public class CrawlerThread extends Thread{
    boolean checkMail(Set<String> hosts, long timeout, TimeUnit unit) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        final AtomicBoolean hasNewMail = new AtomicBoolean(false);
        try {
            for (final String host : hosts) {
                exec.execute(() -> {
                    if (checkMail(host)) {
                        hasNewMail.set(true);
                    }
                });
            }
        }finally {
            exec.shutdown();
            exec.awaitTermination(timeout,unit);
        }
        return hasNewMail.get();
    }
    private boolean checkMail(String host) {
        //do something
        return false;
    }
}

2.4 shutdownNow局限性

通过shutdownNow强行关闭ExecutorService时,它会尝试取消正在执行的任务,并返回所有已提交但尚未开始的任务,从而将这些任务写入日志或者保存起来以便之后进行处理.

public class TrackingExecutor extends AbstractExecutorService {
    private final ExecutorService exec;
    private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet<>());
    public TrackingExecutor(ExecutorService exec) {
        this.exec = exec;
    }
    public List<Runnable> getCancelledTasks() {
        if (!exec.isTerminated()) {
            throw new IllegalStateException();
        }
        return new ArrayList<>(tasksCancelledAtShutdown);
    }
    @Override
    public void execute(final Runnable command) {
        exec.execute(() -> {
            try {
                command.run();
            } finally {
                if (isShutdown() && Thread.currentThread().isInterrupted()) {
                    tasksCancelledAtShutdown.add(command);
                }
            }
        });
    }
    @Override
    public void shutdown() {
        exec.shutdown();
    }
    @Override
    public List<Runnable> shutdownNow() {
        return exec.shutdownNow();
    }
    @Override
    public boolean isShutdown() {
        return exec.isShutdown();
    }
    @Override
    public boolean isTerminated() {
        return exec.isTerminated();
    }
    @Override
    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
        return exec.awaitTermination(timeout,unit);
    }
}
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值