第二部分 结构化并发应用程序
6.任务执行(Task Execution)
6.1 在线程中执行任务
- 当围绕"任务执行"来设计应用程序结构时,第一步就是要找出清晰的任务边界
- 清晰的任务边界以及明确的任务执行策略
- 大多数服务器应用程序都提供了一种自然的任务边界选择方式:以独立的客户请求为边界.Web服务器,邮件服务器,文件服务器,EJB容器以及数据库服务器等.将独立的请求作为任务边界,既可以实现任务的独立性,又可以实现合理的任务规模.
6.1.1 串行的执行任务
- 串行的Web服务器 效率差 还有阻塞
public class SingleThreadWebServer {
public static void main ( String[] args ) throws IOException {
ServerSocket socket = new ServerSocket( 80 );
while ( true ) {
Socket connection = socket.accept();
handleRequest( connection );
}
}
private static void handleRequest ( Socket connection ) {
// request-handling logic here
}
}
6.2 Executor框架
- Tasks are logical units of work, and threads are a mechanism(机制) by which tasks can run asynchronously(异步)
- 把所有任务都放在单个线程中串行执行和为每个任务都创建一个线程运行都有其缺陷
- java类库中,任务执行的主要抽象不是Thread 而是Executor
- Executor以异步方式进行执行
public interface Executor {
void execute( Runnable command );
}
- Executor是个简单的接口,但他为灵活而强大的异步任务执行框架提供了基础.他基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者
- 每当看到
new Thread( runnable ).start()
这种代码都应该使用Executor来代替Thread
-
Executor {void execute(Runnable command);}
-
Executors :Factory and utility methods for Executor, ExecutorService 为其他俩提供工厂方法和utils的,不能实例化.就提供这种方法:newFixedThreadPool(), newCachedThreadPool()等等
-
ExecutorService
public interface ExecutorService extends Executor {
void shutdown();
boolean isShutdown();
boolean isTerminated();
<T> Future<T> submit(Callable<T> task);
...
}
6.2.1 基于Executor的Web服务器
public class TaskExecutionWebServer {
private static final int NTHREADS = 100;
private static final Executor exec = Executors.newFixedThreadPool( NTHREADS );
public static void main ( String[] args ) throws IOException {
ServerSocket socket = new ServerSocket( 80 );
while ( true ) {
final Socket connection = socket.accept();
Runnable task = () -> handleRequest( connection );
exec.execute( task );
}
}
}
6.2.2 执行策略
- 在什么线程中执行任务
- 任务按照什么顺序执行(FIFO LIFO 优先级 这里面说的是单线程吗)
- 有多少个任务能并发执行
- 在队列中有多少个任务在等待执行
- 如果系统由于过载需要拒绝一个任务时,那么应该选择哪一个任务
- 如何通知应用程序有任务别拒绝
- 在执行一个任务前后,应该进行哪些操作
6.2.3 线程池
- newFixedThreadPool
- newCachedtThreadPool
- newSingleThreadExecutor
- newScheduledThreadPool
6.2.4 Executor的生命周期
- 为了解决执行服务的生命周期问题,ExecutorService扩展了Executor接口.shutdown执行平缓的关闭过程(不接受新任务,但是会等正在执行的任务之行为从),shutdownNow执行粗暴的关闭过程(尝试取消所有运行中的任务,不再启动队列中尚未执行的任务)
public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
// ... additional convenience methods for task submission
}
支持关闭操作的Web服务器
class LifecycleWebServer {
private final ExecutorService exec = ...;
public void start() throws IOException {
ServerSocket socket = new ServerSocket( 80 );
while ( !exec.isShutdown() ) {
try {
final Socket conn = socket.accept();
exec.execute( () -> handleRequest( conn ) );
}
catch ( RejectedExecutionException e ) {
if ( !exec.isShutdown() ) {
log( "task submission rejected", e );
}
}
}
}
public void stop () {
exec.shutdown();
}
void handleRequest ( Socket connection ) {
Request req = readRequest( connection );
if ( isShutdownRequest( req ) ) {
stop();
}
else {
dispatchRequest( req );
}
}
}
6.3 找出可利用的并行性
- Callable类似于Runnable 区别在于前者会抛出异常或者返回一个值.Future的结果表示的是the result of an asynchronous computation(用get() retrieve)
interface ArchiveSearcher { String search(String target); }
class App {
ExecutorService executor = ...;
ArchiveSearcher searcher = ...;
void showSearch ( final String target ) throws InterruptedException {
Future< String > future = executor.submit( () -> searcher.search( target ) );
displayOtherThings(); // do other things while searching
try {
displayText( future.get() ); // use future
}
catch ( ExecutionException ex ) {
cleanup();
return;
}
}
}
- FutureTask是Future和Runnalbe的一种实现 利用它可以把上面的代码改成
void showSearch ( final String target ) throws InterruptedException {
FutureTask<String> future =
//new FutureTask<>( () -> searcher.search( target ) )
new FutureTask<String>(new Callable<String>() {
public String call() {
return searcher.search(target);
}});
executor.execute(future);}
displayOtherThings(); // do other things while searching
try {
displayText( future.get() ); // use future
}
}
6.3.2 携带结果的任务Callable与Future
public interface Callable<V> {
V call() throws Exception;
}
``
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException,
CancellationException;
V get( long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException,
CancellationException, TimeoutException;
}
-
Callable相比于Runnable是一种更好的抽象,它认为主入口点(即call方法)将返回一个值,并可能抛出一个异常,在Exceutor中包含了一些辅助方法能将其他类型的任务封装为一个Callable.Exceutor执行的任务有四个生命周期:创建,提交,开始和完成.在Exceutor框架中,已提交的但是尚未开始的任务可以取消,但是对于那些已经开始执行的任务,只有当他们能响应中断时,才能取消.
-
Future表示一个任务的生命周期,可用于异步任务.任务没有完成的时候,get方法将阻塞并直到完成.ExcutorService中的所有submit方法都将返回一个Future,从而将一个Runnable或Callable提交给Executor,并得到一个Future用来获得任务的执行结果或者取消任务.还可以显式的为某个指定的Runnable或Callable实例化一个FutureTask(FutureTask实现了Runnable,因此可以将它提交给Executor来执行或直接调用run()方法)
示例:渲染页面
- 图像下载大部分时间是等待I/O操作完成,cpu基本不工作
串行的渲染页面元素
- 先绘制文本元素,同时为图像留出矩形的占位符,在处理完了第一遍的文本之后,程序开始下载图像,并将它们绘制到相应的占位空间中.这种串行方法没有很好的利用cpu.
public abstract class SingleThreadRenderer {
void renderPage(CharSequence source) {
renderText(source);
List<ImageData> imageData = new ArrayList<ImageData>();
for (ImageInfo imageInfo : scanForImageInfo(source))
imageData.add(imageInfo.downloadImage());
for (ImageData data : imageData)
renderImage(data);
}
interface ImageData {}
interface ImageInfo {
ImageData downloadImage();
}
abstract void renderText(CharSequence s);
abstract List<ImageInfo> scanForImageInfo(CharSequence s);
abstract void renderImage(ImageData i);
}
使用Future等待图像下载
-
为了使页面渲染器实现更高的并发性,首先将任务渲染过程分为两个任务,一个是渲染所有的文本,另一个是下载所有的图像(因为其中一个是CPU密集型,另一个是I/O密集型,所以在单CPU上也能提升性能) 但我们可以做的更好,用户不必等到所有图像都下载完成,而希望看到每当下载一幅图像的时候就显示出来
-
在异构并行化任务重存在的局限:下面使用了两个任务,一个负责渲染文本,一个负责下载图像.如果渲染文本的速度远高于下载图像的,那么最终性能与串行的差别不大,但是代码却复杂了.只有存在大量相互独立且同构的任务可以进行并发处理时,才能体现出性能提升
public abstract class FutureRenderer {
private final ExecutorService executor = Executors.newCachedThreadPool();
void renderPage(CharSequence source) {
final List<ImageInfo> imageInfos = scanForImageInfo(source);
Callable<List<ImageData>> task =
new Callable<List<ImageData>>() {
public List<ImageData> call() {
List<ImageData> result = new ArrayList<ImageData>();
for (ImageInfo imageInfo : imageInfos)
result.add(imageInfo.downloadImage());
return result;
}
};
//先进行下载 即使到开始请求的时候还没有下载完成,那么至少也是开始下载了
Future<List<ImageData>> future = executor.submit(task);
renderText(source);
try {
List<ImageData> imageData = future.get();
for (ImageData data : imageData)
renderImage(data);
} catch (InterruptedException e) {
// Re-assert the thread's interrupted status
Thread.currentThread().interrupt();
// We don't need the result, so cancel the task too
future.cancel(true);
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
interface ImageData {
}
interface ImageInfo {
ImageData downloadImage();
}
abstract void renderText(CharSequence s);
abstract List<ImageInfo> scanForImageInfo(CharSequence s);
abstract void renderImage(ImageData i);
}
使用CompletionService实现页面渲染器
- 为每一幅图片的下载都创建一个独立任务,并在线程池中执行他们
public abstract class Renderer {
private final ExecutorService executor;
Renderer ( ExecutorService executor ) {
this.executor = executor;
}
void renderPage ( CharSequence source ) {
final List< ImageInfo > info = scanForImageInfo( source );
CompletionService< ImageData > completionService =
new ExecutorCompletionService< ImageData >( executor );
for ( final ImageInfo imageInfo : info ) {
completionService.submit( () -> imageInfo.downloadImage() );
}
renderText( source );
try {
for ( int t = 0, n = info.size() ; t < n ; t++ ) {
Future< ImageData > f = completionService.take();
ImageData imageData = f.get();
renderImage( imageData );
}
}
catch ( InterruptedException e ) {
Thread.currentThread().interrupt();
}
catch ( ExecutionException e ) {
throw launderThrowable( e.getCause() );
}
}
abstract void renderText ( CharSequence s );
abstract List< ImageInfo > scanForImageInfo ( CharSequence s );
abstract void renderImage ( ImageData i );
interface ImageData {
}
interface ImageInfo {
ImageData downloadImage ();
}
}
7 取消与关闭
- 任务中应该包含取消策略,线程中应该包含中断策略
- 一个行为良好的软件与勉强运行的软件之间的最主要的区别就是:行为良好的软件能完善的处理失败,关闭和取消过程
7.1 任务取消
- 使用volatile类型的域来保存取消状态,cancelled是volatile变量
while( !cancelled ) {...}
但是 用这种方法需要特别注意是不是在阻塞方法中,否则任务可能永远无法结束. - 当生产者超过消费者太多,队列满了的时候,put方法被阻塞了.这时候虽然消费者可以cancel任务,但是这样他也就不会从队列中取素数来消费了,所以生产者永远不能检测到这个标志,它无法从阻塞的put方法中恢复过来. 解决方法:使用中断,通常中断是实现取消的最合理方式
class BrokenPrimeProducer extends Thread {
private final BlockingQueue< BigInteger > queue;
private volatile boolean cancelled = false;
BrokenPrimeProducer ( BlockingQueue< BigInteger > queue ) {
this.queue = queue;
}
public void run () {
try {
BigInteger p = BigInteger.ONE;
while ( !cancelled ) {
queue.put( p = p.nextProbablePrime() );
}
}
catch ( InterruptedException consumed ) {
}
}
public void cancel () {
cancelled = true;
}
//消费者
void consumePrime() throws InterruptedException {
BlockingQueue< BigInteger > primes = ...;//init
BrokenPrimeProducer producer = new BrokenPrimeProducer(primes);
producer.start();
try {
while ( needMorePrimes() )
} finally {
producer.cancel();
}
}
}
- 通过中断来取消,因为调用了阻塞的put方法,这里并不一定要显示的检测(而且我觉得检测也没用了...这个中断是put方法里响应的把),但是执行检测会让它对中断又更高的响应性,因为它是在启动寻找素数的任务之前检测中断的,而不是任务完成后.如果可中断的阻塞方法的调用频率不高,不足以获得足够的响应性,那么显示的检测中断能起到一定的帮助作用.
public class PrimeProducer extends Thread {
private final BlockingQueue< BigInteger > queue;
PrimeProducer ( BlockingQueue< BigInteger > queue ) {
this.queue = queue;
}
public void run () {
try {
BigInteger p = BigInteger.ONE;
while ( !Thread.currentThread().isInterrupted() ) {
queue.put( p = p.nextProbablePrime() );
}
}
//这边和上面的while语句里的判断条件 做了一个双重的判断,这里接受的异常是put方法抛出来的
catch ( InterruptedException consumed ) {
/* Allow thread to exit */
}
}
public void cancel () {
interrupt();
}
}
7.1.2 中断策略
-
最合理的中断策略是某种形式的线程级(Thread-Level)取消操作或服务级(Service-Level)取消操作:尽快退出,在必要时进行清理,通知某个所有者该线程已经退出
-
最合理的取消策略:尽快退出执行流程,并把中断信息给调用者,从而使调用栈的上层代码可以采取进一步的操作.(抛出InterruptedException作为中断响应)
-
如果无法传递InterruptedException(或许通过Runnable来定义任务),那么需要另一种方法来保存中断请求.一种标准的方式就再次调用Interrupt方法来回复中断状态.
-
平台类库大多数可阻塞方法都同时定义了限时版本和无限时版本例如.Thread.join,BlockingQueue.put,CountDownLatch.await,Selector.select等
7.1.6 处理不可中断的阻塞
不可中断的阻塞,只能设置线程的中断状态,除此之外没有任何作用.对于这些线程,可以使用类似中断的手段来停止这些线程,但这样必须知道线程阻塞的原因
- 获取某个锁:如果线程由于等待某个内置锁而阻塞,那么将无法响应中断.但是在Lock类中提供了lockInterrruptibly方法,该方法允许在等待锁的同时仍能响应中断
- Java.io包中的同步Socket I/O:InputStream和OutptStream中的read和write方法都不会响应中断,但是通过关闭底层的套接字,可以使执行的线程抛出一个SocketException
- Java.io包中的同步I/O 当中断一个正在InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException 并关闭链路.
- Selector的异步I/O
停止基于线程的服务
- 线程由Thread对象表示,并且可以像其他对象一样被共享.然而线程有一个相应的所有者,即创建该线程的类.因此线程池是其工作者线程的所有者,如果要中断这些线程,应该由线程池来(ExecutorService中提供的shutDown,shutdownNow).
- 应用程序会创建线程池,线程池中有工作线程,应用程序不应该直接操作工作线程.
- 对于持有线程的服务(比如线程池),只要服务的存在时间大于创建线程的方法存在的时间,那么就应该提供生命周期方法.