重温《并发编程实战》---任务执行

来源:互联网 发布:淘宝的林国庆 编辑:程序博客网 时间:2024/05/17 23:51

1.无线创建线程的不足:

  

如果串行的执行任务,那么我们的阻塞情况可能会很严重,并且服务器的资源利用率非常低,cpu将处于空间状态。

 

所有有的程序员就为每个请求任务分布一个线程,这的确能带来更快的响应性和更高的吞吐率,但是这同样会带来一些问题。

 

public static void main(String[]args)throws InterruptedException, IOException{

ServerSocket socket = new ServerSocket(80);

while(true){

Socket connection = socket.accept();

Runnable task = new Runnable() {

@Override

public void run() {

handleRequest(connection);

}

};

new Thread(task).start();

}

    }  

 

 

1.)线程生命周期的开销非常高:线程的创建与销毁不是没有代价的,不同的平台,线程的开销也不同但线程的创建都会需要时间,这也会延迟处理的请求,并且需要JVM和操作系统提供一些辅助操作如果请求的到达率非常高(即请求接踵而至)并且请求是很轻量级的任务(即很快就能处理完的任务),那么为每个请求创建一个线程就会消耗大量的计算资源(此时线程创建的时间和线程创建所带来的开销将占据主要部分,因为请求是轻量级)。

 

  2.)资源消耗,活跃的线程会消耗系统资源,尤其是内存。当你的处理器无法保证所有线程都得到运行时间的时候,会产生一些闲置线程,这些闲置线程会占用内存空间,如果你的线程数量越来越多,就会消耗很多的内存空间,这会给垃圾回收器带来压力。

   而且大量的线程在竞争cpu的时候还将产生其他的性能开销(例如上下文切换)。

          如果你已经拥有足够多线程能保证所有cpu都保持忙碌状态,那么再创建更多的线程反而会降低性能。

 

 

       3.)稳定性:在可创建线程的数量上会有一个限制,这个限制值随着平台的不同而不同,包括JVM的启动参数,Thread构造函数中请求的栈大小,操作系统对线程的限制等。

 

 

 

 

 

 

 

 

2.Executor框架:虽然Executor是一个简单的接口,但是它却为强大的异步任务执行策略提供了基础,该框架能支持多种不同类型的任务执行策略。它将任务的提交过程与执行过程解耦开来,并用Runnable表示任务。注意,在Java类库中,任务可以用RunnableCallableFuture来抽象,而任务执行的抽象是用Executor来表达的。

 

 

将任务的提交和任务执行解耦开来的好处:

当我们想换一种执行策略的时候,我们只需要换一个Executor的实现,或者更改下Executor的配置(比如换一个线程池),Executor的实现一半在部署阶段就可以完成,即一次更改,到处执行。

任务提交的代码可能会存在于应用程序的各个地方,如果任务的提交和任务执行不分开,那么我们要在应用程序中的很多地方修改代码,而现在将任务的提交和任务执行分开了,我们只需要换一种Exccutor的实现或更改下它的配置就可以实现新的执行策略。

比如 public void execute(Runnable r){

r.run();

}

此时的执行策略就是单线程执行策略。

如果换成

 

public void execute(Runnable r){

new Thread(r).start;

}

此时执行策略就被换成了为每个任务都创建一个线程。

所以,我们可以通过改变Executor的实现来改变执行策略。

 

 

 

3.Executor静态工厂方法创建线程池:

 

线程池是与工作队列紧密相关的,在工作队列中保存了所有等待执行的任务,线程池从工作队列获取一个任务,执行任务,然后返回线程池,等待下一个任务

newFixedThreadPool:这个静态工厂方法将创建一个固定长度的线程池,每当提交一个任务就创建一个线程,知道达到线程池的最大容量,这时线程池的规模将不再变化。

newCacheedThreadPool:这个静态工厂方法将创建一个可缓存的线程池,如果线程池当前的处理规模超过了处理需求的时候,那么将回收空闲线程,而当需要增加的时候,可以创建新的线程,线程池的规模没有限制。

newSingleThreadExecutor:这个静态工厂方法生成一个单线程的Executor

newScheduledThreadPool:创建了一个固定长度的线程池,而且以延时或者定时的方式执行任务。

看,以上就是4中不同的执行策略,通过改变Executor的配置或实现来改变了执行策略。

 

4.为了解决执行服务的生命周期问题,Executor扩展了ExecutorService接口,添加了一些用于管理生命周期的方法。

 

 

 

 

5.当使用 ExecutorService 完毕之后,我们应该关闭它,这样才能保证线程不会继续保持运行状态。 

当不再提交任何任务时,调用shutdown方法


举例来说,如果你的程序通过 main() 方法启动,并且主线程退出了你的程序,如果你还有1个活动的 ExecutorService 存在于你的程序中,那么程序将会继续保持运行状态。存在于 ExecutorService 中的活动线程会阻止Java虚拟机关闭。
为了关闭在 ExecutorService 中的线程,你需要调用 shutdown() 方法。ExecutorService 并不会马上关闭,而是不再接收新的任务,一旦所有的线程结束执行当前任务,ExecutorServie 才会真的关闭。所有在调用 shutdown() 方法之前提交到 ExecutorService 的任务都会执行。
如果你希望立即关闭 ExecutorService,你可以调用 shutdownNow() 方法。这个方法会尝试马上关闭所有正在执行的任务,并且跳过所有已经提交但是还没有运行的任务。但是对于正在执行的任务,是否能够成功关闭它是无法保证的,有可能他们真的被关闭掉了,也有可能它会壹直执行到任务结束。这是1个最好的尝试。

 

 

6.延迟任务和周期任务:Timer类可以用来管理延时任务和周期任务,但是Timer类存在一些缺陷,Timer在执行所有定时任务时只会创建1个线程,如果某个任务的执行时间过长,那么将破坏其他TimeTask的定时精确性。

 

在现在很少使用Timer,我们可以用ScheduledThreadPoolExecutor来替他它。

如果要构建自己的调度服务,那么可以使用DelayQueue,它实现了BlockingQueue,并为ScheduledThreadPoolExecutor提供调度功能。DelayQueue中,管理着一组Delayed对象,每个Delayed对象都有一个相应的延时时间:在DelayQueue中,只有某个元素逾期后,才能从DelayQueue中执行take操作。从DelayQueue中返回的对象将根据他们的延迟时间进行排序。

 

 

 

 

7.Callable和Runnable的区别:

  http://uule.iteye.com/blog/1488270

 

1.Callable可以返回一个类型V,而Runnable不可以
2.Callable能够抛出checked exception,而Runnable不可以
3.Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的
4.Callable和Runnable都可以应用于executors。而Thread类只支持Runnable.
上面只是简单的不同,其实这两个接口在用起来差别还是很大的。Callable与executors联合在一起,在任务完成时可立刻获得一个更新了的Future。而Runable却要自己处理

 

 

 

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

 

 

9.对于一些需要返回结果和可能抛出异常的任务,Callable<T>可能是一种更好的抽象,如果要使用Callable来表示无返回值的任务,可使用Callable<Void>

 

 

 

10.Future表示的是任务的生命周期,它有用来判断任务状态的方法(isDone(),isCancled()),也有取消任务的方法(cancle()),最重要的是获得结果的方法(get())。如果任务已经完成,get返回结果或抛出一个异常,如果任务没有执行完毕,get将阻塞并直到任务完成。

 

11.get方法的异常:

           a.)ExecutionException:如果任务抛出了异常,get将把异常封装成                                      ExecutionException,想要获得初始异常,可以通过getCause来获得 被封装的初始异常。

           b.)CancelationException:如果任务被取消,那么get将抛出CancellationException。

           c.)InterruptedException:

           d.)TimeOutException ---适用于get(long timeout,TimeUnit unit)方法。

 

 

12.FutureTask:它是Future的实现类,但是它其实是RunnableFuture的实现类,即间接的实现了Runnable,我们可以直接调用它的run方法,也可以将它提交给Executor执行。

 

个人理解:Callable,Runnable,FutureTask都可以当成是任务的抽象,Callable适用于返回结果的任务,FutureTaskRunnable的基础上提供了任务周期的一些方法。

    public FutureTask(Callable<V>callable) {

        if (callable ==null)

            throw new NullPointerException();

        this.callable =callable;

        this.state =NEW;       // ensure visibility of callable

    }

 

    

    public FutureTask(Runnablerunnable, Vresult) {

        this.callable = Executors.callable(runnable,result);

        this.state =NEW;       // ensure visibility of callable

}

 

 

当然,我们也可以显示地为某个执行的Runnable或Callable实例化一个FutureTask。

 

也可以用ThreadPoolExecutor的RunnableFuture<T> newTaskFor(Callable<T> task)的默认实现。

 

    

 

 

 

 

13.相关的安全发布:在将Runnable或Callable提交到Executor的过程中,包含了一个安全发布过程,即将Runnable或者Callable从提交的线程发布到最终执行任务的线程。

                   Future中获得结果的时候,结果也从计算它的线程发布到任何通过get获得它的线程。

14.异构任务并行化的局限性:

        对于异构任务的并行化所能带来的性能提升是很困难的:

        a.)将不同类型的任务平均分配给每个工作线程是很困单的,尤其多个异构任务。

        b.)异构任务中,不同任务的大小也是一个很重要的影响因素。比如我们有2个任务,A,B,

单独完成A需要10个时间单位,B需要1个时间单位,串行的执行A,B需要11个时间单位。并行执行需要10个时间单位,最后我们只节省了1个时间单位,但我们的程序却更加复杂并且带来的性能提升很有限。

 

 

 

        结论:大量相互独立并且同构的任务更比异构任务更适合进行并发的处理,因为这样的性能提升更加显著。

 

 

15.CompletionService(完成服务):

      它的关注点有2个:Executor和BlockingQueue.

      它会将计算任务部分提交给Executor来执行,将已经计算完成的结果放入BlockingQueue,我们可以使用CompletionService的take和poll等方法来从BlockingQueue中获得结果(实际上这两个方法就是委托给BlockingQueue时间的)。

      

      ExecutorCompletionService实现了ComplementService。

      ExecutorCompletionService关注点:当任务被提交的时候,任务首先被封装成1个QueueFuture,它extends FutureTask(控制周期的任务抽象),计算完成的时候会调用done()方法,将自己(this)加入带计算完成的BlockingQueue中,当想获得结果的时候,可以从这个BlockingQueue中取出Future,然后调用Future.get()方法。

 

16.限时任务:

 

在使用限时任务的时候应该注意,当任务超时的时候应该立即停止,从而避免继续浪费资源。要实现这个功能,可以由任务本身管理它的限定时间,并在超时的时候中止执行或者取消任务,这时可以使用Future来实现这个功能,它有一个限时的get()方法,在超时的时候抛出TimeOutException,此时还可以通过Future来取消任务。

17.InvokeAll方法:它的方法参数接受一组任务,并返回一组Future,它支持限时的版本。

 

 

18.如果需要提交一个任务集并等待他们完成,那么可以使用ExecutorService.invokeAll,并且使用CompletionService来获取结果。

 

List<Task<Object>> tasks = new ArrayList<Task<Object>>();

BlockingQueue<Future<Object>>futureQueue =new ArrayBlockingQueue<Future<Object>>(10);

ExecutorCompletionService<Object> resultFrom =new ExecutorCompletionService<Object>(e,futureQueue);

executorService.invokeAll(tasks);

Object  result = resultFrom.take();

 

原创粉丝点击