1 线程池
如果直接使用new Thread来创建线程,其中存在着一些问题:
-
通过new Thread来新建对象性能差;
-
线程缺乏统一管理,可能无限制新建线程,还可能占用过多系统内存而导致死机或内存溢出
-
缺乏了定时执行、定期执行、线程中断等功能。
Java中给我们提供了线程池来管理线程,在jdk1.5版本之后的java.util.concurrent包中有介绍线程以及线程池的使用。
接下来我们看下,相比较直接new Thread来新建线程,使用线程池的好处有哪些:
-
重用存在的线程, 降低创建和销毁线程造成的消耗,性能佳。
-
通过调整线程池中并发线程数目,防止内存消耗过大导致的死机。
-
提供定时执行、定期执行、单线程、并发数控制等功能。
2 线程池相关的类
Java线程池最顶端的接口是Executors,但严格来讲Exceutors不是一个线程池,而是一个执行线程的工具,真正的线程池接口是ExecutorService。
java.util.concurrent包中比较重要的几个类:
类 | 作用 |
---|---|
ExecutorService | 真正的线程池接口。对Executors接口的扩展,提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制 |
ScheduledExecutorService | 能和Timer/TimerTask类型,解决需要任务重复执行的问题 |
ThreadPoolExecutor | ExecutorService的实现类 |
ScheduledThreadPoolExecutor | 继承ThreadPoolExecutor和ScheduledExecutorService接口实现,周期性任务调度的类实现 |
3 创建线程池的方法
(1)newFixedThreadPool
创建固定大小的线程池。线程数目没有达到线程池最大值的之前,每次提交一个任务就创建一个线程。线程池的大小一旦达到最大值就会保持不变,如果某个线程异常结束,那么线程池会补充一个新线程。
代码演示:
ExecutorService executor = Executors.newFixedThreadPool(3);
for(int i = 0; i < 4; i++) {
executor.execute( new Runnable() {
@Override
public void run() {
CountDownLatch countDownLatch = new CountDownLatch(1); //计数器,用来阻塞线程
System.out.println(Thread.currentThread().getName() + "正在执行");
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
(2)newCachedThreadPool
创建一个可缓存的线程池(也叫无界线程池)。该线程池不会对线程大小做限制,该线程池大小完全依赖操作系统(或者说JVM)能够创建线程的最大值。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
代码演示:
ExecutorService executor = Executors.newCachedThreadPool();
for(int i = 0; i < 4; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
CountDownLatch countDownLatch = new CountDownLatch(1); //计数器,用来阻塞线程
System.out.println(Thread.currentThread().getName() + "正在执行");
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
(3)newSingleThreadExecutor
创建一个单线程的线程池。该线程池只有一个线程在工作,也相当于单线程串行执行所有任务。如果这个唯一的线程异常结束,就会有一个新的线程来替代它。此线程池保证所有任务的执行顺序都是一定的。
代码演示:
ExecutorService executor = Executors.newSingleThreadExecutor();
for(int i = 0; i < 4; i++) {
final int index = i;
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行---->"+ index);
}
});
}
(4)newScheduledThreadPool
创建一个大小无限的线程池。该线程池支持定时以及周期性执行任务的需求。
代码演示:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
for(int i = 0; i < 6; i++) {
final int index = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "延时2s后,每6s执行一次任务--- >"+ index);
}
};
executor.scheduleAtFixedRate(runnable, 2, 6, TimeUnit.SECONDS);
}
4 线程池底层实现
就拿newFixedThreadPool创建线程池的实现作为例子。下面是java.util.concurrent.Executors的newFixedThreadPool方法:
public static ExecutorService newFixedThreadPool(int nThreads) {
//调用到ThreadPoolExecutor类的构造方法(可以说ThreadPoolExecutor是Executors类的底层实现。)
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
ThreadPoolExecutor类提供了四个构造方法,其余三个都是在这个构造方法的基础上产生,其中ThreadPoolExecutor类的完整构造方法如下:
public ThreadPoolExecutor(int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //当线程数大于核心时,终止前多余的空闲线程等待新任务的最长时间。
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue, //用于存储的队列
ThreadFactory threadFactory, //创建线程的工厂
RejectedExecutionHandler handler) //饱和策略,当超出最大线程池数量时候执行该策略
{
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
3.1 ThreadPoolExecutor饱和策略定义:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经放满了任时,ThreadPoolTaskExecutor定义一些策略:
-
ThreadPoolExecutor.AbortPolicy:抛出
RejectedExecutionException
来拒绝新任务的处理。 -
ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用
execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 -
ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
-
ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
举个“栗子”: Spring 通过 ThreadPoolTaskExecutor
或者我们直接通过 ThreadPoolExecutor
的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler
饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy
。在默认情况下,ThreadPoolExecutor
将抛出 RejectedExecutionException
来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy
。当最大池被填满时,此策略为我们提供可伸缩队列。
3.2 线程池的队列
以下是BlockingQueue的实现类:
- SynchronousQueue:同步队列,一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。
- LinkedBlockingQueue:链阻塞队列,无界缓存等待队列。当前执行的线程数量达到核心线程数(corePoolSize)的数量时,剩余的元素会在阻塞队列里等待,内部以 FIFO(先进先出)的顺序对元素进行存储。
- ArrayBlockingQueue:数组有界阻塞队列(先进先出),按照阻塞的先后顺序访问队列,默认情况下不保证线程公平的访问队列。当线程数已经达到最大的最大线程数(maximumPoolSize )时,再有新的元素尝试加入ArrayBlockingQueue时会报错。
- PriorityBlockingQueue:优先级队列,可以自定义排序方法,但对于同级元素不能保证顺序
- DelayQueue:延迟获取元素队列,只有在延迟期满时才能从中提取元素,为无界阻塞队列。