Java并发核心基础——线程池使用及底层实现机制详解

来源:互联网 发布:windows打印服务 编辑:程序博客网 时间:2024/06/05 02:17

Java线程池概述:

从使用入手:
java.util.concurrent.Executosr是线程池的静态工厂,我们通常使用它方便地生产各种类型的线程池,主要的方法有三种:
1、newSingleThreadExecutor()——创建一个单线程的线程池
2、newFixedThreadPool(int n)——创建一个固定大小的线程池
3、newCachedThreadPool()——创建一个可缓存的线程池
 
1、SingleThreadExecutor
特点:单线程串行工作,如果这个唯一的线程因为异常终止,则有一个新的线程来替代它。
2、FixedThreadPool
特点:固定大小的线程池,如果设定的所有线程都在运行,新任务会在任务队列等待。
3、CachedThreadPool
特点:大小可伸缩的线程池。如果当前没有可用线程,则创建一个线程。在执行结束后缓存60s,如果不被调用则移除线程。调用execute()方法时可以重用缓存中的线程。适用于很多短期异步任务的环境,可以提高程序性能。

以CachedThreadPool为例:
public class MyCachedThreadPool {public static void main(String[] args){//创建Runnable对象,实现run()方法Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " running!");try {//为了体现出任务竞争资源,让线程休眠1000msThread.sleep(1000L);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " end!");}};//创建线程池,加入任务,执行任务ExecutorService myThreadPool = Executors.newCachedThreadPool();myThreadPool.execute(runnable);myThreadPool.execute(runnable);myThreadPool.execute(runnable);myThreadPool.execute(runnable);myThreadPool.execute(runnable);//关闭线程池myThreadPool.shutdown();}}
测试结果:


其中,我们不光会使用excute()方法执行任务,还会使用submit()方法,submit方法主要适用于使用Callable的情况,区别主要有如下几点:
1、接收参数不一样,excute()方法需要一个Runnable类型参数,而submit方法需要一个Callable<T>类型参数。
2、返回值不同,excute()方法没有返回值,submit方法会返回Future<T>类型返回值。
3、submit方法适合处理异常。执行的任务里会抛出checked或者unchecked exception,而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。

使用线程池的好处:
1、降低资源消耗。重复利用已创建线程,降低线程创建与销毁的资源消耗。
2、提高响应效率。任务到达时,不需等待创建线程就能立即执行。
3、提高线程可管理性。
4、防止服务器过载。内存溢出、CPU耗尽。

线程池应用范围:
1、需要大量的线程来完成任务,并且完成所需时间较短。如Web服务器完成网页请求。
2、对性能有苛刻要求。如服务器实时响应。
3、突发性大量请求,且不至于在服务器产生大量线程。

介绍线程池实现机制之前:

关于线程池一些比较重要的类或接口:
(1)ExecutorService是真正的线程池接口,所以我们在通过Excutors创建各种线程时,一般采用如下代码:
ExecutorService threadPool = Executors.newXXX();
先声明一个线程池接口,运行时动态绑定具体的线程池对象,典型的接口的用法(虽然ExecutorService名字起的不像个接口……)。
从这段代码还可以看出,(2)Executors是静态工厂的功能(虽然名字也不像个工厂……),生产各种类型线程池。
需要注意的是,要区分(3)Executor与Executors,Executor是线程池的顶级接口,但它只是一个执行线程的工具,真正的线程池接口是ExecutorService。
(4)AbstractExecutorService实现了ExecutorService接口,实现了其中大部分的方法(有没有实现的,所以被声明为Abstract)。
(5)ThreadPoolExecutor,继承了AbstractExecutorService,是ExecutorService的默认实现,也是一会将要介绍的重点。
这五个类或接口实现了从线程池顶层接口到底层实现的整个架构。

其它的带有Schedule关键字的类或接口与实现周期性重复性工作相关,不是本篇考虑的重点。类图如下:


除了继承Thread、实现Runnable、Callable三种创建线程方式外的第四种创建方式:
实现java.util.concurrent.ThreadFactory接口,实现newThread(Runnable r)方法
这种方式应用于这样一种场景:我们需要一个线程池,并且对于线程池中的线程对象、赋予统一的名字、优先级,以及一些其他统一操作,使用这样的工厂方式就是优秀程序员应该使用的最好的方法。

线程池工作机制及原理:

先看看Excutors创建各种线程池的源代码:
1、newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {   return new FinalizableDelegatedExecutorService   (new ThreadPoolExecutor(1, 1,                                   0L, TimeUnit.MILLISECONDS,                                   new LinkedBlockingQueue<Runnable>()));   }
2、newFixedThreadPool(int n)
public static ExecutorService newFixedThreadPool(int nThreads) {   return new ThreadPoolExecutor(nThreads, nThreads,     0L, TimeUnit.MILLISECONDS,                                     new LinkedBlockingQueue<Runnable>());   }
3、newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {   return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                     60L, TimeUnit.SECONDS,                                     new SynchronousQueue<Runnable>());   }
发现所有实现都是创建了一个ThreadPoolExecutor实例(ThreadPoolExecutor是ExecutorServiec默认实现类,加深印象)

其中ThreadPoolExecutor的构造函数中有几个参数,现在介绍这些参数,是理解线程池工作原理的重要方式:

1、第一个参数:int corePoolSIze,核心池大小,也就是线程池中会维持不被释放的线程数量。我们可以看到FixedThreadPool中这个参数值就是设定的线程数量,而SingleThreadExcutor中就是1,newCachedThreadPool中就是0,不会维持,只会缓存60L。但需要注意的是,在线程池刚创建时,里面并没有建好的线程,只有当有任务来的时候才会创建(除非调用方法prestartAllCoreThreads()与prestartCoreThread()方法),在corePoolSize数量范围的线程在完成任务后不会被回收。
2、第二个参数:int maximumPoolSize,线程池的最大线程数,代表着线程池中能创建多少线程池。超出corePoolSize,小于maximumPoolSize的线程会在执行任务结束后被释放。此配置在CatchedThreadPool中有效。
3、第三个参数:long keepAliveTime,刚刚说到的会被释放的线程缓存的时间。我们可以看到,正如我们所说的,在CachedThreadPool()构造过程中,会被设置缓存时间为60s(时间单位由第四个参数控制)。
4、第四个参数:TimeUnit unit,设置第三个参数keepAliveTime的时间单位。
5、第五个参数:存储等待执行任务的阻塞队列,有多种选择,分别介绍:

SynchronousQueue——直接提交策略,适用于CachedThreadPool。它将任务直接提交给线程而不保持它们。如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求最大的 maximumPoolSize 以避免拒绝新提交的任务(正如CachedThreadPool这个参数的值为Integer.MAX_VALUE)。当任务以超过队列所能处理的量、连续到达时,此策略允许线程具有增长的可能性。吞吐量较高。

LinkedBlockingQueue——无界队列,适用于FixedThreadPool与SingleThreadExcutor。基于链表的阻塞队列,创建的线程数不会超过corePoolSizes(maximumPoolSize值与其一致),当线程正忙时,任务进入队列等待。按照FIFO原则对元素进行排序,吞吐量高于ArrayBlockingQueue。

ArrayListBlockingQueue——有界队列,有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

ThreadPoolExcutor的构造方式不仅有这一种,总共有四种,还可以在最后加入一个参数以控制线程池任务超额处理策略:
当用来缓存待处理任务的队列已满时,又加入了新的任务,那么这时候就该考虑如何处理这个任务。
可以通过实现RejectedExceptionHandler接口,实现rejectedException(ThreadPoolExecutor e, Runnable r)方法自定义操作。但通常我们使用JDK提供了4种处理策略,在ThreadPoolExecutor构造时以参数传入:
ThreadPoolExcutor.AbortPolicy()——直接抛出异常,默认操作
ThreadPoolExcutor.CallerRunsPolicy()——只用调用者所在线程来运行任务
ThreadPoolExcutor.DiscardOldersPolicy()——丢弃队列里最近的一个任务,并执行当前任务
ThreadPoolExcutor.DiscardPolicy()——不处理,直接丢掉

关于excute()方法,它的执行实际上分了三步:
1、当少量的线程在运行,线程的数量还没有达到corePoolSize,那么启用新的线程来执行新任务。
2、如果线程数量已经达到了corePoolSize,那么尝试把任务缓存起来,然后再次检查线程池的状态,看这个时候是否能添加一个额外的线程,来执行这个任务。如果这个检查到线程池关闭了,就拒绝任务。
3、如果我们没法缓存这个任务,那么我们就尝试去添加线程去执行这个任务,如果失败,可能任务已被取消或者任务队列已经饱和,就拒绝掉这个任务。
可以参考源码理解:
public void execute(Runnable command) {    if (command == null)        throw new NullPointerException();    if (workerCountOf(c) < corePoolSize) {        if (addWorker(command, true))            return;        c = ctl.get();    }    if (isRunning(c) && workQueue.offer(command)) {        int recheck = ctl.get();        if (! isRunning(recheck) && remove(command))            reject(command);        else if (workerCountOf(recheck) == 0)            addWorker(null, false);    }    else if (!addWorker(command, false))        reject(command);}
其中,线程池会把每个线程封装成一个Worker对象,由addWorker(Runnable firstTask, boolean core)方法控制,firstTask代表线程池首要执行的任务,core代表是否使用corePoolSize参数作为线程池最大标记。



参考资料:
《Java并发编程从入门到精通》
http://www.oschina.net/question/565065_86540
http://www.cnblogs.com/whthomas/p/java-executors.html
http://www.cnblogs.com/wanqieddy/p/3853863.html

1 0
原创粉丝点击