Java并发:三种线程抽象以及线程执行

本文详细介绍了Java中的线程抽象,包括Runnable、Callable和FutureTask。重点讲解了ExecutorService的生命周期管理和ScheduledExecutorService的延迟及周期性执行。还提到了使用CompletionService简化多任务结果获取的问题,以及如何将Runnable和Callable包装成FutureTask以实现任务的取消和结果获取。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 转载请注明出处: jiq钦's technical Blog - 季义钦


在Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,但是Runnable任务没有返回值,而Callable任务有返回值。此外还有一个FutureTask任务类型以拓展Runnable和Callable。


(备注:图片转自网络)


一、Runnable任务抽象

考虑一个处理客户端请求的服务器程序:


1.1 Thread方式

通常我们为了提高服务器监听程序的响应能力,并不会在监听主程序中处理接收到的客户端请求,而是在接收到请求之后为其分配一个线程去处理:

class ThreadPerTaskWebServer{
         publicstatic void main(String[] args) throws IOException{
                   ServerSocketsocket = new ServerSocket(8080);
                   while(true){
                            fnalSocket connnection = new socket.accept();
                            Runnabletask = new Runable(){ //匿名类
                                     publicvoid run(){
                                               handleClientRequest(connection);
}
};
new Thread(task).start();
//or rask.run();
}
}
}

1.2 线程池

1.2.1 线程池抽象Executor

生产环境中,“为每个请求分配一个线程”这种方法存在一定缺陷,尤其是当需要创建大量线程时,主要体现在几个方面:

A 频繁创建和销毁线程代价非常高;

B 过多线程极耗内存。当你有足够多的线程保持CPU忙碌时,更多的线程反而降低性能;

C 稳定性问题。可创建线程数受JVM参数、栈大小等限制,无限制创建线程可能带来异常;

 

由此java推出了线程池的库,任务执行的抽象从Thread变为ExecutorExecutor描述的是一个具备特定执行策略的线程池,应用程序只需要简单地将任务提交到线程池,具体的执行策略由具体的线程池实现决定。

class TaskExecutionWebServer{
         privatestatic final int SIZE = 100;
         privatestatic final Executor exec = Executors.newFixedThreadPool(SIZE);
         publicstatic void main(String[] args) throws IOException{
                   ServerSocketsocket = new ServerSocket(8080);
                   while(true){
                            finalSocket connection = socket.accept();
                            Runnabletask = new Runable(){
                                     publicvoid run(){
                                               handleClientRequest(connection);
}
};
exec.execute(task);
}
}
}

Executor接口较为简单,仅仅包含一个执行接口:

public interface Executor {
    voidexecute(Runnable command);  //特别注意仅仅支持执行Runnable
}

注意我们可以采用不同的Executor实现,来改变服务器执行任务的策略。

比如我们实现一个和前面一样无限制创建线程并启动的Executor线程池:
public class ThreadPerTaskExecutorimplements Executor {
         publicvoid execute(Runnable r){
                   newThread(r).start();
}
}

1.2.2 带生命周期管理的线程池ExecutorService

为了解决执行服务的生命周期问题,Executor扩展出了ExecutorService接口,添加了一些生命周期管理的方法。并且能够返回一个可以追踪异步任务进度的Future对象。

public interface ExecutorService extends Executor {
    voidshutdown();  
   List<Runnable> shutdownNow();   
    booleanisShutdown();  
    booleanisTerminated(); 
    booleanawaitTermination(long timeout, TimeUnit unit) throws InterruptedException;  
    <T>Future<T> submit(Callable<T> task);  //返回任务的Future,可取消和获取结果
    Future<?>submit(Runnable task);  //返回任务的Future,可取消
}

ExecutorService创建时处于运行状态。当调用ExecutorService.shutdown()后,处于关闭状态,isShutdown()方法返回true。这时,不应该再向ExecutorService中添加任务,所有已添加的任务执行完毕后,ExecutorService处于终止状态,isTerminated()返回true。

 

异步计算结果Future

对于一个提交到线程池的任务,其执行可能需要很长的时间,有时候希望能够取消这些任务。Future表示一个指定任务的生命周期,提供了对应的方法判断一个任务是否已经完成或取消,以及提供获取任务的结果取消任务的方法。

public interface Future<V> {
    booleancancel(boolean mayInterruptIfRunning);
    booleanisCancelled();
    boolean isDone();
    V get() throwsInterruptedException, ExecutionException;
    V get(longtimeout, TimeUnit unit)
        throwsInterruptedException, ExecutionException, TimeoutException;
}

任务可取消条件:

1、  已提交尚未开始执行的任务

2、  已经开始执行的任务需要能够响应中断才能够取消

 

1.2.3 延迟和周期性执行的线程池ScheduledExecutorService

ScheduledExecutorService拓展了ExecutorService接口,增加类似于Timer的功能,而且也解决了Timer的缺陷,比如Timer在执行所有定时任务时只有一个线程,如果某个任务时间较长,那么将破坏其他TimerTask的定时精确性。另外如果TimerTask抛出一个未检查的异常,Timer并不捕获异常,此时定时器线程将终止。

public interface ScheduledExecutorService extendsExecutorService {
    publicScheduledFuture<?> schedule(Runnable command, long delay, TimeUnitunit);   
    public <V>ScheduledFuture<V> schedule(Callable<V> callable, long delay,TimeUnit unit);
}

1.2.4 Executors工具类

Java提供了一个工具类Executors来获取上述默认的线程池实现。

public static ExecutorService newFixedThreadPool(int nThreads)

创建固定数目线程的线程池。

public static ExecutorService newCachedThreadPool()

创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。

如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

public static ExecutorService newSingleThreadExecutor()

创建一个单线程化的Executor。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

 

二、Callable任务抽象

Executor框架使用Runnable作为其基本的任务表示形式。但是Runnable有很大局限:

1、 不能返回一个值

2、 不能抛出一个受检查的异常

 

实际上许多任务都存在延迟的计算:执行数据库查询、从网络上获取资源、或者计算某个复杂的功能,对于这些任务,一个带返回值的Callable任务是一个更好的抽象。它认为任务的主入口即call()将返回一个值,或者可能抛出一个异常。


2.1 ExecutorService执行

<执行一个带返回值的Callable任务>

ExecutorService线程池提供如下方法提交一个Callable任务:

<T> Future<T> submit(Callable<T> task);

 

下面是一个页面渲染的例子,将页面渲染分为两个任务:主线程渲染文本,另一个线程渲染所有下载的图像。

public class FutureRender{
         privatefinal ExecutorService executor = . . . ;
         voidrenderPage(CharSequence source){
                   finalList<ImageInfo> imageInfos = scanForImage(source);
                   Callable<List<Image>>task = new Callable<List<Image>>(){
                            publicList<Image> call(){
                                     List<Image>result = new ArrayList<Image>();
                                     for(ImageInfoinfo : imageInfos)
                                               result.add(info.downloadImage());//串行下载所有图像
                                     returnresult;                              
}
};
 
Future<List<Image>>future = executor.submit(task); //提交任务
renderText(source);
 
try{
         List<Image>imageData = future.get(); //阻塞获取任务执行结果
         for(Image image :imageData)
                   renderImage(image);
}catch(InterruptedException ee)
{
         Thread.currentThread().interrupt();//重新设置中断状态
         future.cancel(true);  //线程被中断,所以取消下载任务
} catch(Exception ee)
{
        
}
}
}

2.2 CompletionService执行

<执行一组待返回值的Callable任务>

当待返回值的任务抽象Callable交给ExecutorService线程池来执行时,某些场景下将会不适用。考虑这样的场景,如果需要向Executor提交一组计算Callable任务,并且希望在计算完成之后获取所有计算任务的计算结果,比如上面渲染页面的例子中,不是开启一个计算任务下载所有图像,而是针对每个图像都开启一个下载任务。

这个时候就需要维护所有提交的下载图像的计算任务的Future引用,在获取结果时循环获取这一组计算任务的执行结果。

 

很明显这样比较繁琐,因此Java提供了CompletionService,将ExecutorBlockingQueue的功能封装在一起,我们尽管将一组Callable计算任务都提交给CompletionService,只需要维护一个CompletionService线程池对象,最后需要这组任务的计算结果时,只需要通过这个维护的CompletionService对象循环取出(出队)即可。

 

CompletionService提供take方法按完成顺序返回任务的结果,CompletionService内部维护了一个阻塞队列BlockingQueue,如果没有任务完成,take()方法也会阻塞。

public interface CompletionService<V> {
    Future<V>submit(Callable<V> task);
    Future<V>submit(Runnable task, V result);
    Future<V>take() throws InterruptedException;
    Future<V>poll();
    Future<V>poll(long timeout, TimeUnit unit) throws InterruptedException;
}

ExecutorCompletionService是CompletionService的一个具体实现类,可以看到其构造函数要求传入一个Executor线程池,ExecutorCompletionService就是为传入的线程池封装利用队列存储一组任务的执行结果的功能。

public class ExecutorCompletionService<V> implementsCompletionService<V> {
         publicExecutorCompletionService(Executor executor) {
}

再看上面那个例子,将其改为针对每个图像的下载都分配一个计算任务:

public class FutureRender{
         privatefinal ExecutorService executor = . . . ;
         voidrenderPage(CharSequence source){
                   finalList<ImageInfo> imageInfos = scanForImage(source);
                  
                   //封装executor
                   CompletionService<Image>completionService = new
                            ExecutorCompletionService<Image>(executor);
                  
                   //为每个图像下载都提交一个Callable任务
                   for(final ImageInfoinfo : imageInfos)
                            completionService.submit(newCallable<Image>(){
                                     publicList<Image> call(){
                                               returninfo.downloadImage();
                                     }
});
 
renderText(source);
 
try{
         for(inti=0,n=imageInfos.size();i<n;i++){
                   Future<Image>f = completionService.take(); //将完成的Future取出
                   Image image = f.get(); //获取计算结果
                   renderImage(image);
}
}catch(InterruptedException ee)
{
         Thread.currentThread().interrupt();//重新设置中断状态
         future.cancel(true);  //线程被中断,所以取消下载任务
} catch(Exception ee)
{
        
}
}
}

三、FutureTask任务抽象

FutureTask描述一个可取消的异步计算任务。

public class FutureTask<V> implementsRunnableFuture<V> {
    publicFutureTask(Callable<V> callable) { }
    publicFutureTask(Runnable runnable, V result) { }
}

其中RunnableFuture接口继承自Runnable和Future接口:

public interface RunnableFuture<V> extends Runnable,Future<V> {
    void run();
}

从类定义也可以看出,FutureTask是另一种Runnable抽象,这种Runnable抽象具备Future的可获取执行结果和可取消的特性。

Runnable和Callable都可以包装成FutureTask,让其变为Runnable和Future类型。

3.1 将Callable包装成FutureTask

让Callable任务既可以作为Runnable被执行,又可以具备Future的可取消、可获取执行结果的特性。

特别注意:让Callable任务可以作为Runnable任务执行,代表其可以交给自定义Executor线程池执行,也可以像Runnable那样直接start或者run。

3.2 将Runnable包装成FutureTask

让Runnable任务既可以作为Runnable被执行,又可以具备Future的可取消的特性。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值