多线程实践之任务执行

任务执行

1、线程中执行任务
1.1、串行地执行任务

在应用程序中存在多种调度策略,最简单的一种是在单个线程中串行地执行各个任务。

    class SingleThreadWebServer{
        public static void main(String[] args)throws IOException{
            ServerSocked socket = new ServerSocket(80);
            while(true){
                Socket connection = socket.acept();
                handleRequest(connection);
            }
        }
    }

这种调度策略在访问量较低时,时可以应付的。但是这种机制无法提供高吞吐率和快速响应性。

1.2、为任务创建线程

通过为每一个请求任务创建一个新的线程来提供服务,从而实现更高的响应性。

    class SingleThreadWebServer{
        public static void main(String[] args)throws IOException{
            ServerSocked socket = new ServerSocket(80);
            while(true){
                final Socket connection = socket.accept();
                Runnable task = new Runnable(){
                    public void run(){
                        handleRequest(connection);
                    }
                }
                new Thread(task).start();
            }
        }
    }

在正常负载下,“为每个任务分配一个线程”的方法能提升串行执行的性能。只要请求的到达率不超过服务器的请求处理能力,那么这种方法就可以带来更高的吞吐率和更快的响应性。

1.3、无线创建线程的代价
  1. 线程生命周期的开销非常高(新线程将消耗大量的计算资源)
  2. 资源消耗(消耗系统资源,尤其是内存)
  3. 稳定性(最大线程数量,不同的平台可能不同)

综上所述,虽然效果有改进,但是都不尽人意。

2、Executor框架

Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者。

2.1、示例:基于Executor的Web服务器

基于前边的代码,这里创建一个固定长度的线程池,可以容纳100个线程。

    class TaskExecutionWebServer{
        private static fianl int NTHREADS = 100
        private static fianl Executor exec = Executor.newFixedThreadPool(NTHREADS);
        
        public static void main()throws IOExeception{
            ServerSocket socket = new ServerSocket(80);
            while(true){
                final Socket connection = socket.accept();
                Runnable task = new Runnable(){
                    public void run(){
                         handleRequest(connection);
                    }
                };
                exec.execute(task);
            }
        }
    }

线程池是指管理一组同构工作线程的资源池。工作队列中保存着所有等待执行的任务,工作者线程的任务很简单:从工作队列中获取一个任务,执行任务,人后返回线程池并等待下一个任务。这里的线程是重复利用的,不是每次新建的。

创建线程池的方法:

  • newFixedThreadPool 创建固定长度的线程池。
  • newCachedThreadPool 创建可缓存的线城市
  • newSingleThreadExecutor创建单个Executor
  • newScheduledThreadPool 创建固定长度的线程池,以延迟或者定时的方式执行任务。
2.2、执行策略
  • 在什么(what)线程中执行。
  • 任务按照什么(what)顺序执行(FIFO?)
  • 有多少个(how many)任务能并发执行。
  • 在队列中有多少个(how many)任务等待执行。
  • 如果系统过载需要拒绝任务,那么应该选择(which)哪一个任务?如何(how)通知应用程序有任务被拒绝。
  • 在一个任务执行之前和之后应该进行哪些(what)动作。
2.3、生命周期

由于Executor以异步方式来执行任务,因此在任何时刻,之前提交的任务的转台不是立即可见的。有些任务可能已经完成,有些可能正在执行,而其他的任务可能在队列中等待执行。当应用程序关闭时,可能采用最平缓的关闭方式(完成所有已经启动的任务,并且不再接受任何新的任务),也可能采用最粗暴的方式(关闭电源),为了解决生命周期的问题,Executor扩展了ExecutorService接口,添加了一下生命周期的管理方法。具体的可以查看ExecutorService。

    //平缓的关闭方式
    void shutdown();
    //粗暴的关闭方式
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();//轮训是否终止
    boolean awaitTermination(long timeout,TimeUnit unit);
    ....
    
2.4、周期任务

Timer类负责管理延迟任务以及周期任务,但是存在一些缺陷,应该考虑使用ScheduledThreadPoolExecutor来替代它。如果要构建自己的调度服务,那么可以使用DelayQueue。它实现了BlockingQueue,并为ScheduledThreadPoolExecutor提供调度功能。

3、优化实例

假设现在有一个页面需要我们去渲染,这个页面包含HTML标签,预定大小的图片和URL。

3.1、串行页面渲染

最简单的处理方法就是对HTMl页面进行串行处理。

    public class SingleThreadRenderer{
        void renderPage(CharSquence source){
            renderText(source);
            List<ImageData> imageData = new ArrayList<ImageData>();
            for(ImageInfo imageInfo : scanForImageInfo(source)){
                imageData.add(imageInfo.downloadImage());
            }
            for(ImageData data:imageData){
                renderImage(data);
            }
        }
    }

这种方式的大部分时间都是在等待I/O操作执行完成,CPU在这期间几乎不做任何工作。因此如果能够分开执行,将会获得更高的效率。

3.2、携带结果的Callable和Future

Executor框架中,已提交但未开始的任务可以取消,但是对于那些已经开始执行的任务,只有他们能响应中断时,才能取消。

为了使页面的渲染速度提高,首先将渲染过程分解为两个任务,一个是渲染所有的文本,另一个是下载所有的图像。(因为一个是CPU密集型,一个是I/O密集型)Callable和Future有助于表示这些协同人物之间的交互

    public class FutureRenderer{
        private final ExecutorService executor =...;
        
        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)
            renderTask(source);
            
            try{
                //如果任务完成,get会抛出异常;如果没完成,get将阻塞。
                List<ImageData> imageData = future.get();
                for(ImageData:imageData)
                    renderImage(data);
            }catch(InterruptedException e){
                //重新设置线程中断状态
                Thread.currentThread().interrupt();
                //由于不需要结果,取消任务
                future.cancel(true);
            }catch(Exeception e){
            
            }
        }
    }
3.3、CompletionService

如果Executor提交一组任务,希望得到计算结果,可以保留与每个任务关联的Future,然后反复使用get,同时将timeout指定为0,通过轮询判断任务是否完成,但是这种凡是过于繁琐,这里哟更好的方法。

CompletionService将Executor和BlockingQueue融合在一起。你可以将Callablle任务提交给他,然后使用队列操作的take方法和poll等方法来获取已经完成的结果。

总结

Executor框架将任务提交与执行策略解耦开来,同时还支持多种不同类型的执行策略。当需要创建线程来执行任务时,可以考虑使用Executor。要想在将应用程序分解为不同的任务时获得最大好处,必须定义清晰的任务边界。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值