Java并发编程实战——结构化并发应用程序2

7 取消与关闭

Java没有提供任何机制来安全地终止线程。但是Java提供了中断(interruption)这种协作机制。

7.1 任务取消

取消某个操作的原因:

  • 用户请求取消
  • 有时间限制的操作
  • 应用程序事件。例如,应用程序对某个问题空间进行分解并搜索,当其中一个任务找到了解决方案时,所有其他仍在搜索的任务都将被取消。
  • 错误。例如爬虫程序,当一个爬虫任务发生错误时(例如磁盘空间已满),那么所有的搜索任务都会取消,此时可能记录它们的当前状态,以便稍后重新启动。
  • 关闭。在平缓的关闭过程中,当前正在执行的任务将继续执行直到完成;在立即关闭的过程中,当前的任务则可能取消。

程序清单 7-1-1 使用volatile类型的域来保存取消状态


import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

@ThreadSafe
public class PrimeGenerator implements Runnable {
    @GaurdedBy("this")
    private final List<BigInteger> primes = new ArrayList<>();
    private volatile boolean canceled;

    @Override
    public void run() {
        BigInteger p = BigInteger.ONE;
        while (!canceled) {
            p = p.nextProbablePrime();
            synchronized (this) {
                primes.add(p);
            }
        }
    }

    public void cancel() {
        this.canceled = true;
    }

    public synchronized List<BigInteger> get() {
        return new ArrayList<>(primes);
    }
}

程序清单 7-1-2 一个仅运行一秒的素数生成器

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

        return generator.get();
    }

程序清单 7-1-1中的代码是有问题的。如果素数生成器中调用的是阻塞式的方法,任务可能永远不会检查取消标志,因此永远不会结束。

7.1.1 中断

在Java的API或语言规范中,并没有将中断和任何取消语义关联起来,但实际上,如果在取消之外的其他操作中使用中断都是不合适的,并且很难支撑起更大的应用。

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

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

程序清单 7-1-1-1 通过中断来取消

import java.math.BigInteger;
import java.util.concurrent.BlockingQueue;

public class PrimeProducer extends Thread {
    private final BlockingQueue<BigInteger> queue;

    PrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while (!Thread.currentThread().isInterrupted()) {
                queue.put(p = p.nextProbablePrime());
            }
        } catch (InterruptedException consumed) {
            /* 允许线程退出 */
        }
    }

    public void cancel() {
        interrupt();
    }
}

7.1.2 中断策略

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

7.1.3 响应中断

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

对于一些不支持取消但仍可以调用可中断阻塞方法的操作,它们必须在循环中调用这些方法,并在发现中断后重新尝试。在这种情况下,应该在本地保存中断状态,并在返回前恢复状态而不是在捕获InterruptedException时恢复状态。
程序清单 7-1-3-1 不可取消的任务在退出前恢复中断

    public Task getNexeTask(BlockingQueue<Task> queue) {
        boolean interrupted = false;
        try {
            while (true) {
                try {
                    return queue.take();
                } catch (InterruptedException e) {
                    interrupted = true;
                    // 重新尝试
                }
            }
        } finally {
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
        }
    }

7.1.4 示例:计时运行

 private static final ScheduledExecutorService cancelExec = ...;
    
    public static void timedRun(final Runnable r, long timeout, TimeUnit unit) throws InterruptedException {
        class RethowableTask implements Runnable {
            private volatile Throwable t;
            
            @Override
            public void run() {
                try {
                    r.run();   
                } catch (Throwable t) {
                    this.t = t;
                }
            }
            
            void rethrow() {
                if (t != null) {
                    throw launcherThrowable(t);
                }
            }
        }
        
        RethowableTask task = new RethowableTask();
        final Thread taskTread = new Thread(task);
        taskTread.start();
        cancelExec.schedule(new Runnable() {
            @Override
            public void run() {
                taskTread.interrupt();
            }
        }, timeout, unit);
        taskTread.join(unit.toMillis(timeout));
        task.rethrow();
    }

7.1.5 通过Future来实现取消

    private static final ScheduledExecutorService taskExec = ...;

    public static void timedRun(final Runnable r, long timeout, TimeUnit unit) throws InterruptedException {
        Future<?> task = taskExec.submit(r);
        try {
            task.get(timeout, unit);
        } catch (TimeoutException e) {
            // 接下来任务将被取消
        } catch (ExecutionException e) {
            throw launcherThrowable(e.getCause());
        } finally {
            // 如果任务已经结束,那么执行取消操作也不会带来任何影响
            // 如果任务正在运行,那么将被中断
            task.cancel(true);  
        }
    }

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

7.1.6 处理不可中断的阻塞

java.io包中的同步socket I/O: 虽然InputStream和OutputStream中的读写等方法都不会响应中断,但是通过关闭底层套接字,可以使得由于执行读写方法而被阻塞的线程抛出一个SocketException。

java.io包中的同步I/O: 当中断一个正在IntteruptibleChannel上等待的线程时,将抛出ClosedByInterruptException并关闭链路(其他在这条链路上阻塞的线程也会抛出ClosedByInterruptException)。当关闭一个InterruptibleChannel时,将导致所有在链路上阻塞的线程都抛出AsynchronousCloseException。大多数标准的channel都实现了InterruptibleChannel。

Selector的异步I/O: 如果一个线程在调用Selector.select方法的时候阻塞了,那么调用close或者wakeup方法会使线程抛出ClosedSelectorException并提前返回。

获取某个锁: 如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求。但是,Lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁的同时仍能响应中断。

程序清单 7-1-6-1 通过改写interrupt方法将非标准的取消操作封装在Thread中

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class ReaderThread extends Thread{
    private final Socket socket;
    private final InputStream in;

    public ReaderThread(Socket socket) throws IOException {
        this.socket = socket;
        this.in = this.socket.getInputStream();
    }

    @Override
    public void interrupt() {
        try {
            socket.close();
        } catch (IOException ignored) {

        } finally {
            super.interrupt();
        }
    }

    @Override
    public void run() {
        try {
            byte[] buf = new byte[BUFSZ];
            while (true) {
                int count = in.read(buf);
                if (count < 0) {
                    break;
                } else if (count > 0) {
                    processBuffer(buf, count);
                }
            }
        } catch (IOException e) {
            // 允许线程退出            
        }
    }
}

7.1.7 采用newTaskFor来封装非标准的取消

public interface CancellableTask<T> extends Callable<T> {
    void cancel();
    RunnableFuture<T> newTask();
}
public class CancellingExecutor extends ThreadPoolExecutor {
    @Override
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        if (callable instanceof CancellableTask<T>) {
            return ((CancellableTask) callable).newTask();
        } else {
            return super.newTaskFor(callable);
        }
    }
}
import java.net.Socket;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;

public abstract class SocketUsingTask<T> implements CancellableTask<T> {
    private Socket socket;

    protected synchronized void setSocket(Socket socket) {
        this.socket = socket;
    }

    public synchronized void cancel() {
        try {
            if (socket != null)
                socket.close();
        } catch (Exception exception) {

        }
    }

    @Override
    public RunnableFuture<T> newTask() {
        return new FutureTask<>(this) {
            @Override
            public boolean cancel(boolean mayInterruptIfRunning) {
                try {
                    SocketUsingTask.this.cancel();
                } finally {
                    return super.cancel(mayInterruptIfRunning);
                }
            }
        };
    }
}

7.2 停止基于线程的服务

线程的所有权是不可以传递的:应用程序可以拥有服务,服务也可以拥有工作者线程,但是应用程序并不能拥有工作者线程,因此应用程序不能直接停止工作者线程。

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

7.2.1 示例:日志服务

public class LogWriter {
    private final BlockingQueue<String> queue;
    private final LoggerThread logger;

    public LogWriter(Writer writer) {
        this.queue = new LinkedBlockingQueue<>();
        this.logger = new LoggerThread((PrintWriter) writer);
    }

    public void start() {
        logger.start();
    }

    public void log(String msg) throws InterruptedException {
        queue.put(msg);
    }

    private class LoggerThread extends Thread {
        private final PrintWriter writer;

        public LoggerThread(PrintWriter writer) {
            this.writer = writer;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    writer.println(queue.take());
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                writer.close();
            }
        }
    }
}
public class LogService {
    private final BlockingQueue<String> queue;
    private final LoggerThread loggerThread;
    private final PrintWriter writer;
    private boolean isShutdown;
    private int reservations;

    public void start() {
        loggerThread.start();
    }

    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) {

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

7.2.2 关闭ExecutorService

public class LogService {
    private final ExecutorService exec = Executors.newSingleThreadExecutor();
    
    public void start() {}
    
    public void stop() throws InterruptedException {
        try {
            exec.shutdown();
            exec.awaitTermination(timeout, unit);
        } finally {
             writer.close();
        }
    }
    
    public void log(String msg) {
        try {
            exec.execute(new WriteTask(msg));    
        } catch (RejectedExecutionException e) {
            
        }
    }
}

7.2.3 毒丸对象

毒丸对象的含义是:当得到这个对象时,立即停止。毒丸对象保证消费者在关闭线程之前完成队列中的所有工作。 生产者提交了毒丸对象之后,将不会再提交任何工作。

只有生产者消费者数量已知的情况下,才可以使用毒丸(Poison Pill)对象。并且只有在无界队列中,毒丸对象才可以可靠的工作。

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

程序清单 7-2-4-1 使用私有的Executor,并且该Executor的生命周期受限于方法调用

 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(new Runnable() {
                    @Override
                    public void run() {
                        if (checkMail(host))
                            hasNewMail.set(true);
                    }
                });
            }
        } finally {
            exec.shutdown();
            exec.awaitTermination(timeout, unit);
        }

        return hasNewMail.get();
    }

7.2.5 shutdownNow的局限性

public class TrackingExecutor extends AbstractExecutorService {
    private final ExecutorService exec;
    private final Set<Runnable> tasksCanceledAtShutdown = Collections.synchronizedSet(new HashSet<>());

    public List<Runnable> getCanceledTasks() {
        if (!exec.isTerminated())
            throw new IllegalStateException();
        return new ArrayList<>(tasksCanceledAtShutdown);
    }

    @Override
    public void execute(Runnable command) {
        exec.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    command.run();
                } finally {
                    if (isShutdown() && Thread.currentThread().isInterrupted()) {
                        tasksCanceledAtShutdown.add(command);
                    }
                }
            }
        });
    }

    // 其他方法全部委托给exec
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值