`
足至迹留
  • 浏览: 485379 次
  • 性别: Icon_minigender_1
  • 来自: OnePiece
社区版块
存档分类
最新评论

<进阶-5> 线程池的原理和使用

阅读更多
一、线程池Executor
大多数并发应用程序都是围绕“任务执行(Task Execution)”来构造的:任务通常是一些抽象的离散的工作单元。通过把应用程序的工作分解到多个任务中,可以简化程序的组织结构。

当围绕“任务执行”来设计应用程序结构时,第一步就是找出清晰的任务边界,理想情况下,各个任务是相互独立的:任务不依赖于其他任务的状态,结果或边界效应。

大多数服务器应用程序都提供了一种自然的任务边界选择方式:以独立的客户请求为边界。Web服务器,邮件服务器,文件服务器,EJB容器以及数据库服务器等,都能通过网络接受远程客户的连接请求。将独立的请求作为任务边界,既可以实现任务的独立性,又可以实现合理的任务规模。

在服务器应用程序中,串行处理机制通常都无法提供高吞吐率或快速响应性。也有一些例外,如当任务数量很少且执行时间很长时,或当服务器只为单个用户提供服务,并且该客户每次只发出一个请求时,但大多数应用程序并不是按照这种方式来工作的。
可以通过为每个请求创建一个新的线程来提供服务,从而实现更高的响应性,如果每个线程调用的是同一个业务处理方法,那这个方法必须设计为线程安全的。在正常负载情况下,“为每个任务分配一个线程”的方法能提升串行执行的性能。只要请求的到达速率不超出服务器的请求处理能力,那么这种方法可以同时带来更快的响应性和更高的吞吐率。

在生产环境中,“为每个任务分配一个线程”这种方法存在一些缺陷,尤其是当需要创建大量的线程时:
1) 线程生命周期的开销非常高。
2) 资源消耗。活跃的线程会消耗系统资源,尤其是内存;大量空闲的线程也会占用许多内存,给垃圾回收器带来压力。而且大量线程在竞争cpu资源时还将产生其他的性能开销。
3) 稳定性。在可创建线程的数量上存在一个限制,这个限制值随平台的不同而不同。


在一定范围内,增加线程可以提高系统的吞吐率,但如果超出了这个范围,再创建更多的线程只会降低程序的执行速度,并且如果过多创建线程整个程序可能崩溃。

1.1 executor 框架
任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。通过前面所有的介绍,我们已经见过了两种执行任务的策略,即把所有任务放在单个线程中串行执行,以及将每个任务放在各自的线程执行。这两种方式都存在一些严格的限制:串行执行的问题在于糟糕的响应性和吞吐率,而“为每个任务分配一个线程”的问题在于资源管理的复杂性。
前面我们介绍生产者-消费者模式时知道了有界队列可以用来防止高负荷的应用程序耗尽内存。类似,线程池简化了线程的管理工作,并且java.util.concurrent提供了一种灵活的线程池实现作为Executor框架的一部分。在java类库中,任务执行的主要抽象不是Thread,而是Executor.

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the <tt>Executor</tt> implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution.
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}


Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者。如果要在程序中实现一个生产者-消费者的设计,最简单的方式通常就是使用Executor.
每当看到new Thread(runnable).start()时,并且希望获得一种更灵活的执行策略时,请考虑使用Executor来代替Thread.

下面是线程池的整体类图框架,先大概了解,后面还会分别讨论:


请参考:
http://www.cnblogs.com/jersey/archive/2011/03/30/2000231.html

1.2 线程池
线程池,从字面含义来看,是指管理一组同构工作线程的资源池。线程池是与工作队列(Work Queue)密切相关的,其中在工作队列中保存了所有等待执行的任务。工作线程(Work Thread)的任务很简单:从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。“在线程池”中执行任务比“为每个任务分配一个线程”优势更多。通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和撤销过程中产生的巨大开销,而且任务来到时不需要等待创建线程。
类库提供了一个灵活的线程池以及一些有用的默认配置。可以通过调用Executors(注意,Executors是静态工厂类,不是接口Executor)中的静态工厂方法来创建各种不同的线程池。
1.newFixedThreadPool. 创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,知道达到线程池的最大量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)。
2.newCachedThreadPool。创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。线程池的规模不存在任何限制。
3.newSingleThreadExecutor. 是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常技术,会创建另一个线程来替代。此线程池能确保依照任务在队列中的顺序来串行执行(如FIFO,LIFO,优先级)。
4.newScheduledThreadPool。创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务。

关于阻塞队列,强烈建议参考:http://www.oschina.net/question/565065_86540
这个链接里主要讨论了三种阻塞队列的使用场景。设置不恰当会导致SynchronousQueue丢弃任务请求;LinkedBlockingQueue通常是不限制请求数量;ArrayBlockingQueue通常是有界队列,防止资源耗尽,但使用比较复杂,不好把握,jdk不建议使用。本文后面还会讨论阻塞队列。

1.3 Executor的生命周期
我们已经知道如何创建一个Executor,但并没有讨论如何关闭它。Executor的实现通常会创建线程来执行任务,但JVM只有在所有(非守护)线程全部终止后才会退出,因此,如果无法正确关闭Executor,那么jvm将无法结束。

为了解决执行服务的生命周期问题,Executor扩展出了ExecutorService接口,添加了一些用于生命周期管理的方法(同时还有一些用于任务提交的遍历方法)。

ExecutorService的生命周期有3中状态:运行,关闭和已终止。ExecutorService在初始创建时处于运行状态。
shutdown方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行完成,包括那些还未开始执行的任务。
shutdownNow方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。
在ExecutorService关闭后提交的任务将由“拒绝执行处理器(Reject Execution Handler)”来处理,它会抛弃任务,或者使得execute方法抛出一个未检查的RejectedExecutionException。等所有任务都完成后,ExecutorService将转入终止状态。可以调用awaitTermination来等待ExecutorService到达终止状态,或者通过isTerminated来轮询ExecutorService是否已经终止。通常在调用awaitTermination之后会立即调用shutdown,从而产生同步地关闭ExecutorService的效果。

1.4 找出可利用的并行性
Executor框架帮助指定执行策略,但如果要使用Executor,必须将任务表述为一个Runnable。Executor框架使用Runnable作为其基本的任务任务表示形式,Runnable是一种有很大局限的抽象,它不能返回一个值或抛出一个受检查的异常。
许多任务实际上都是存在延迟的计算(执行数据库查询,网络上获取资源等),对于这些任务,Callable是一种更好的抽象:它的主入口点(call()方法,类似Runnable的run()方法)将返回一个值,并可能抛出一个异常。
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

Runnable和Callable描述的都是抽象的计算任务,这些任务通常是有范围的,即都有一个明确的起始点,并且最终会结束。Executor执行的任务有4个生命周期阶段:创建,提交,开始和完成。由于有些任务可能要执行很长时间,因此通常希望能取消这些任务。在Executor框架中,已提交但尚未开始的任务可以取消,但对于那些已经开始执行的任务,只有当他们能响应中断时才能取消。取消一个已经完成的任务不会有任何影响。
Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。Future的get方法可以用来获取任务的结果。Get方法的行为取决于任务的状态(尚未开始,正在运行,已完成)。如果任务已经完成,那么get会立即返回或者抛出一个Exception;如果任务没有完成,那么get将阻塞并直到任务完成。如果任务抛出了异常,那么get将该异常封装为ExecutionException并重新抛出,这时可以通过getCause来获得被封装的初始异常。

可以通过许多种方法创建一个Future来描述任务。ExecutorService中所有submit方法都将返回一个Future,从而将一个Runnable或Callable提交给Executor,并得到一个Future用来获取任务的执行结果或取消任务。还可以显式地为某个指定的Runnable或Callable实例化一个FutureTask(由于FutureTask实现了Runnable,因此可以将它提交给Executor来执行或直接调用它的run方法)。

FutureTask的使用可以参考:
经典用法,使用Callable<T>或Runnable实例创建FutureTask,然后获取任务执行状态或结果:
http://blog.csdn.net/kaiwii/article/details/6773971

高级用法,使用FutrueTask提高缓存性能:
http://my.oschina.net/u/866190/blog/177021

在将Runnable或Callable提交到Executor的过程中,包含另一个安全发布过程,即将Runnable或Callable从提交线程发布到最终执行任务的线程。类似地,在设置Future结果的过程中也包含了一个安全发布,即将这个结果从计算它的线程发布到任何通过get获得它的线程。

1.5 CompletionService:Executor与BlockingQueue
如果向Executor提交了一组计算任务,并且希望在计算完成后获得结果,那么可以保留与每个任务关联的Future,然后反复调用get方法,同时将参数timeout指定为0,从而通过轮询来判断任务是否完成。这种方法虽然可行,但却有些繁琐。幸运的是,还有一种更好的方法:完成任务(CompletionService). CompletionService将Executor和BlockingQueue的功能融合在一起,可以将Callable任务提交给他来执行,然后使用类似对垒操作的take和poll等方法来获得已完成的结果,而这些结果会在完成时封装成Future。ExecutorCompletionService实现了CompletionService,并将计算部分委托给一个Executor。

1.6 为任务设置时限
有时候,如果某个任务无法在指定时间内完成,那么将不再需要它的结果,此时可以放弃这个任务。Future的V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; 方法支持这个功能,当抛出TimeoutException后,通过Future来取消任务,因为继续执行已经没有意义。

二、线程池的使用
2.1 任务与任务执行之间的隐性耦合
Executor框架可以将任务的提交与任务的执行策略解耦开来,但并非所有的任务都能适用所有的执行策略。有些类型的任务需要明确地指定执行策略。
1.依赖性任务。大多数行为正确的任务都是独立的,他们不依赖于其他任务的执行时序,执行结果或其他效果。当在线程池中执行独立的任务时,可以随意地改变线程池的大小和配置,这些修改只会对性能产生影响;然而,如果提交给线程池的任务需要依赖其他的任务,那就隐含地给执行策略带来了约束,此时就必须小心的维持这些执行策略以避免产生活跃性问题。
2.使用线程封闭机制的任务。与线程池相比,单线程的Executor能够对并发性作出更强的承诺。他们能确保任务不会并发的执行,能放宽代码对线程安全的要求。对象可以封闭在任务线程中,使得在该线程中执行的任务在访问对象时不需要同步。
3.对响应时间敏感的任务。如果将一个运行时间较长的任务提交到单线程的Executor中,或将多个运行时间较长的任务提交到一个只包含少量线程的线程池中,那么将降低Executor管理的服务的响应性。
4.使用ThreadLocal的任务。只有当线程本地值的生命周期受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义,而在线程池中的线程中不应该使用ThreadLocal在任务之间传递值。也就是说如果任务执行完而ThreadLocal里的对象仍然有效时就需要显式清除,这个ThreadLocal在一个线程中使用完必须remove掉,不能被分配到其他线程时被其他线程使用。

只有当任务都是同类型的并且相互独立时,线程池的性能才能达到最佳
如果将运行时间较长的与运行时间较短的任务混合在一起,那么除非线程池很大,否则可能造成拥塞,因为很有可能在线程池中运行的都是运行时间较长的任务,其他任务得不到响应;
如果提交的任务依赖于其他任务,那么除非线程池无限大,否则可能造成死锁。
将可以并行进行的方法(任务)放入线程的run(或call)方法里执行,然后把线程放入线程池就可以实现线程池调度任务了。

2.2 配置ThreadPoolExecutor
ThreadPoolExecutor为一些Executor提供了基本的实现,这些Executor是由Executors中的newCachedThreadPool,newFixedThreadPool和newScheduledThreadExecutor等工厂方法返回的。ThreadPoolExecutor是一个灵活的,稳定的线程池,允许通过它的构造函数进行各种定制。
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)


2.2.1 线程的创建与销毁
线程池的基本大小(Core Pool Size),最大大小(Maximum Pool Size)以及存活时间等因素共同负责线程的创建与销毁。基本大小也就是任务池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。线程池的最大大小表示可同时活动的线程数量的上限。如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池当前大小超过了基本大小时,这个线程将被终止。

通过调节线程池的基本大小和存活时间,可以帮助线程池回收空闲线程占有的资源,从而使得这些资源可以用于执行其他工作。newFixedThreadPool工厂方法将线程池的基本大小和最大大小设置为参数中指定的值,而且创建的线程池不会超时。newCachedThreadPool工厂方法把线程池的最大大小设置为Integer.MAX_VALUE,而将基本大小设置为0,并将超时设置为1分钟,这种方法创建出来的线程池可以被无限扩展,并且需求降低时会自动收缩。其他形式的线程池可以通过显式的ThreadPoolExecutor构造函数来构造。

2.2.2 管理队列任务
在有限的线程池中会限制可并发执行的任务数量。(单线程的Executor是一种值得注意的特例:他们能确保不会有任务并发执行,因为他们通过线程封闭来实现线程安全性。)
ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有3种:无界队列,有界队列和同步移交(Synchronous Handoff)。队列的选择与其他的配置参数有关,例如线程池的大小等。newFixedThreadPool和newSingleThreadExecutor在默认情况下使用一个无界的LinkedBlockingQueue。如果所有工作者线程都处于忙碌状态,那么任务将在队列中等候。如果任务持续快速到达,并且超多了线程池处理他们的速度,那么队列将无限制地增加。
一种更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue,有界的LinkedBlockingQueue,PriorityBlockingQueue。有界队列有助于避免资源耗尽的情况发生,但它有带来了新问题:当队列填满后,新的任务怎么办?在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节。如果线程池较而队列较大,那么有助于减少内存使用量,降低cpu的使用率,同时还可以减少上下文切换,但付出的代价可能会限制吞吐量。
对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者提交给工作者线程。

对于Executor,newCachedThreadPool工厂方法是一种很好的默认选择,它能提供比固定大小的线程池更好的排队性能。当需要限制当前任务的数量以满足资源管理需求时,那么可以选择固定大小的线程池。只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程池或队列就可能导致“饥饿”死锁问题。此时应该使用无界的线程池,比如newCachedThreadPool。

2.2.3 饱和策略
当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改。如果某个任务被提交到一个已被关闭的Executor时也会用到饱和策略。JDK提供了几种不同的RejectedExecutionHandler实现。每种实现都包含有不同的饱和策略:AbortPolicy,CallerRunsPolicy,DiscardPolicy和DiscardOldestPolicy。
“终止(Abort)”策略是默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。当新提交的任务无法保存到队列中等待执行时,“抛弃(Discard)”策略会悄悄抛弃该任务。“抛弃最旧的(Discard-Oldest)”策略则会抛弃下一个被执行的任务,然后尝试重新提交新的任务。“调用者运行(Caller-Runs)”策略实现了一种调节机制,该策略不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,在调用者处执行该任务,由于执行任务需要一定的事件,因此主线程至少在一段时间内不会提交新任务,从而减低新任务的流量。

当创建Executor时,可以选择饱和策略或者对执行策略进行修改。如下给出了如何创建一个固定大小的线程池,同时使用“调用者运行”饱和策略:
ThreadPoolExecutor executor = new ThreadPoolExecutor(N_THREADS, N_THREADS,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(CAPACITY));
Executor.setRejectedExecutionHandler(new ThreadPoolExecutors.CallerRunnsPolicy());

2.2.4 在调用构造函数后再定制ThreadPoolExecutor
在调用完ThreadPoolExecutor的构造函数之后,仍然可以通过设置函数(Setter)来修改大多数传递给它的构造函数的参数(如线程池的基本大小,最大大小,存活时间,线程工厂及拒绝执行处理器)。如果Executors是通过Executors中的某个工厂方法创建的(newSingleThreadExecutor除外),那么可以将结果的类型转换为ThreadPoolExecutor以访问设置器。


2.3 扩展ThreadPoolExecutor线程池
ThreadPoolExecutor是可以扩展的,它提供了几个可以在子类化中改写的方法:beforeExecute,afterExecute和terminated,这些方法可以用于扩展ThreadPoolExecutor的行为。
在执行任务的线程中将调用beforeExecute和afterExecute等方法,在这些方法中可以添加日志,计时,监视或统计信息收集的功能。无论任务是从run中正常返回,还是抛出一个异常而返回,afterExecute都会被调用。如果任务在完成后带有一个Error,那么就不会调用afterExecute。如果beforeExecute抛出一个RuntimeException,那么任务将不被执行,并且afterExecute也不会被调用。
在线程池完成关闭操作时调用terminated,也就是在所有任务都已经完成并且所有工作者线程都已经关闭后,terminated可以用来释放Executor在其生命周期里分配的各种资源,在此还可以执行发送通知,记录日志或收集finalize统计信息等操作。
示例:给线程池添加统计信息
  • 大小: 58.3 KB
  • 大小: 176.3 KB
  • 大小: 114.2 KB
0
0
分享到:
评论

相关推荐

    C# CLR原理与线程池详解

    深入理解CLR线程池等原理,是.net编程高手进阶必经之路。。。本教程可以帮助你来了解CLR中的一些令人激动的特性。理解这些特性将更好的帮助你来理解CLR。

    Java进阶教程,面试大全

    线程池的种类,区别和使用场景。 分析线程池的实现原理和线程的调度过程。 线程池如何调优,最大数目如何确认。 ThreadLocal原理,用的时候需要注意什么。 CountDownLatch和CyclicBarrier的用法,以及相互之间的差别...

    Java进阶教程,面试大全,包罗万象

    线程池的种类,区别和使用场景。 分析线程池的实现原理和线程的调度过程。 线程池如何调优,最大数目如何确认。 ThreadLocal原理,用的时候需要注意什么。 CountDownLatch和CyclicBarrier的用法,以及相互之间的差别...

    java多线程视频教程(共七套)

    03. 【中级原理】高并发编程原理和线程池精通教程 04、【高级原理】Java并发多线程编程基础原理与实战 05、【高级原理】【高级原理实战】Java并发编程与高并发解决方案(完整无密) 06、【深度进阶】【高级原理实战】...

    Android代码-Tamic_Retrofit

    实现用async-http 自定义的Retrofit 网络框架,用来进阶学习了解Retrofit内部原理 实现技术 反射,依赖注入,代理, 建造者模式,线程池队列, 接口回调等 用法 配置gradle &gt;compile 'com.tamic:tamicLibrary:1.0.2'...

    Android开发艺术探索

    《Android开发艺术探索》是一本Android进阶类书籍,采用理论、源码和实践相结合的方式来阐述高水准的Android应用开发要点。《Android开发艺术探索》从三个方面来组织内容。第一,介绍Android开发者不容易掌握的一些...

    Android开发艺术探索.任玉刚(带详细书签).pdf

    本书是一本Android进阶类书籍,采用理论、源码和实践相结合的方式来阐述高水准的Android应用开发要点。本书从三个方面来组织内容。第一,介绍Android开发者不容易掌握的一些知识点;第二,结合Android源代码和应用层...

    易语言真正的线程池简易实现例子-易语言

    5.多线程间的许可证的使用:见易语言帮助文档 6.原子锁操作:http://baike.baidu.com/view/6235756.htm (链接仅供参考 ) 一.了解线程池的运行原理: 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后...

    android开发艺术探索高清完整版PDF

    《Android开发艺术探索》是一本Android进阶类书籍,采用理论、源码和实践相结合的方式来阐述高水准的Android应用开发要点。《Android开发艺术探索》从三个方面来组织内容。第一,介绍Android开发者不容易掌握的一些...

    java面试题.docx

    企业常见java面试题,java基础,java进阶 JDK 和 JRE 有什么区别? == 和 equals 的区别是什么? 两个对象的 hashCode()相同,则 equals()也一定为 true,对吗? final 在 java 中有什么作用? java 中操作字符串...

    2020美团技术年货-合集(前端+后台+数据+算法+运维).pdf

    前端 1 移动端UI一致性解决方案 1 美团外卖Flutter动态化实践 26 美团开源 Logan Web:前端日志在 ...Java线程池实现原理及其在美团业务中的实践 245 美团万亿级 KV 存储架构与实践 276 Java中9种常见的CMS GC问题分析

    基于python selenium实现B站直播弹幕和礼物信息爬虫源码+项目操作说明.zip

    (2)加速去重方法:使用线程池,对弹幕和礼物列表同时去重。 (3)运行时长控制:分为两种模式,运行指定时长和运行至直播间关闭。 (4)抓取监控:每进行一次抓取并去重后,使用print输出一次数据列表,以...

    完结13章一课掌握Java并发编程精髓

    该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 3. 阻塞(BLOCKED):表示线程阻塞于锁

    国内面试leetcode-Android-Interview:Android-面试

    一个很棒的安卓专家面试题和答案 历时6个多月,从十多个顶级面试库和...7、Android进阶:进程间通信、Binder、AIDL、AMS/WMS、事件分发、滑动冲突、View绘制流程、性能优化、重要Android源代码和开源库分析。 8、Andr

    leetcode下载-Android_Interviews:Android_Interviews

    7、Android进阶:进程间通信、Binder、AIDL、AMS/WMS、事件分发、滑动冲突、View的绘制流程、性能优化、重要的Android源码和开源库分析等等。 8、Android高新技术:模块化、插件化、组件化、热更新实现原理等等。 9...

Global site tag (gtag.js) - Google Analytics