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() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " running!");
try {
//为了体现出任务竞争资源,让线程休眠1000ms
Thread.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并发编程从入门到精通》