多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担。线程本身也要占用内存空间,大量的线程会占用内存资源并且可能会导致Out of Memory。即便没有这样的情况,大量的线程回收也会给GC带来很大的压力。为了避免重复的创建线程,线程池的出现可以让线程进行复用。通俗点讲,当有工作来,就会向线程池拿一个线程,当工作完成后,并不是直接关闭线程,而是将这个线程归还给线程池供其他任务使用。
线程池整体结构
Executor是线程池最上层的接口,这个接口有一个核心方法execute(Runnable command),具体是由ThreadPoolExecutor类实现,用于执行任务,ExecutorService接口继承Executor,提供了shutdown(),shutdownNew(),submit()等用来关闭和执行线程的方法。
ExecutorService最终的默认实现类ThreadPoolExecutor。
ThreadPoolExecutor分析
首先通过构造器来一步一步分析。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
corePoolSize:线程池的核心池大小,在创建线程池之后,线程池默认没有任何线程,创建之后,默认线程池线程数量为0,当任务过来时就会创建一个新的线程,直到达到corePoolSize之后,会将线程放入workQueue中
-
maximumPoolSize:最大线程数量
-
workQueue一个阻塞队列,用来存储等待执行的任务,当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列。通过workQueue,线程池实现了阻塞功能
-
threadFactory:线程工厂,用来创建线程
-
handler:表示当拒绝处理任务时的策略
-
keepAliveTime:线程池维护线程锁允许的空闲时间,当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程之外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
这些参数是怎么再具体执行中发挥作用的呢,我们来看下
execute的执行流程图
当任务过来时就会创建一个新的线程,直到达到corePoolSize之后,会将线程放入workQueue中, 如果运行线程小于corePoolSize,即使有空闲线程,当任务过来时也会创建新的线程 如果线程池数量大于corePoolSize但小于maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务
如果运行线程数量大于等于maximumPoolSize时,这时如果workQueue也已经满了,则通过handler所指定的策略去处理,如果除核心线程之外的线程没有处理任务,且超过keepAliveTime,就会被回收。
四种常用线程池
01 newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
-
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
-
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
-
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
02 newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
-
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到线程池池队列中。
-
FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
03 newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
一个单线程的线程池,只有一个线程在工作(如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。)能够保证所有任务的执行顺序按照任务的提交顺序执行,同一时段只有一个任务在运行。
04 newScheduleThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
返回值是ScheduledExecutorService
创建一个定长的线程池,而且支持定时的以及周期性的任务执行。
可定时运行(初始延时),运行频率(每隔多长时间运行,还是运行成功一次之后再隔多长时间再运行)的线程池
适合定时以及周期性执行任务的场合。
这里有个执行方法需要注意的。
private void threadPoolExecutorTest6() throws Exception {
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
// 周期性执行某一个任务,线程池提供了两种调度方式,这里单独演示一下。测试场景一样。
// 测试场景:提交的任务需要3秒才能执行完毕。看两种不同调度方式的区别
// 效果1: 提交后,2秒后开始第一次执行,之后每间隔1秒,固定执行一次(如果发现上次执行还未完毕,则等待完毕,完毕后立刻执行)。
// 也就是说这个代码中是,3秒钟执行一次(计算方式:每次执行三秒,间隔时间1秒,执行结束后马上开始下一次执行,无需等待)
threadPoolExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务-1 被执行,现在时间:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
// 效果2:提交后,2秒后开始第一次执行,之后每间隔1秒,固定执行一次(如果发现上次执行还未完毕,则等待完毕,等上一次执行完毕后再开始计时,等待1秒)。
// 也就是说这个代码钟的效果看到的是:4秒执行一次。 (计算方式:每次执行3秒,间隔时间1秒,执行完以后再等待1秒,所以是 3+1)
threadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务-2 被执行,现在时间:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
}
阻塞队列
在线程池中,如果任务数量超过了核心线程数,就会把任务放入阻塞队列等待运行,在线程池中主要有三种阻塞队列
-
ArrayBlockingQueue :基于数组的有界队列。
-
LinkedBlockingQueue:基于链表的先进先出队列,是无界的。
-
SynchronousQueue:无缓冲等待队列,它将任务直接交给线程处理而不保持它们。如果不存在可用于立即运行任务的线程(即线程池中的线程都在工作),则试图把任务加入缓冲队列将会失败,因此会构造一个新的线程来处理新添加的任务,并将其加入到线程池中。
四种拒绝策略
-
AbortPolicy 丢弃任务,并抛出RejectedExecutionException 异常
-
CallerRunsPolicy:该任务被线程池拒绝,由调用 execute方法的线程执行该任务。
-
DiscardOldestPolicy :抛弃队列最前面的任务,然后重新尝试执行任务。
-
DiscardPolicy:丢弃任务,不过也不抛出异常。