线程池、forkjoin的原理分析
什么是线程池
在Java中,如果每个请求到达就创建一个新线程,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。
如果在一个JVM里面创建太多线程,可能会使系统由于过度消耗内存或”切换过度“而导致系统资源紧张。
为了解决这个问题,就有了线程池的概念,线程池的核心逻辑是提前创建好若干个线程放在一个容器中。如果有任务需要处理,则将任务直接分配给线程池中的线程来执行就行,任务处理完以后这个线程不会被销毁,而是等待后续分配任务,同时通过线程池来重复管理线程还可以避免创建大量线程增加开销。
线程池的优势
合理的使用线程池,可以带来一些好处:
- 减低创建线程和销毁线程的性能开销
- 提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行
- 合理的设置线程池大小可以避免因为线程数超过硬件资源瓶颈带来的问题。
Java中提供的线程池API
要想合理的使用线程池,那么势必要对线程池的原理有比较深的理解。
线程池的使用
JDK为我们提供了几种不同的线程池实现。我们先来通过一个简单的案例来引入线程池的基本使用。
在Java中怎么创建线程池呢?下面这段代码演示了创建三个固定线程数的线程池:
public class ThreadPoolTest implements Runnable {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
static ExecutorService service = Executors.newFixedThreadPool(3);
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
service.execute(new ThreadPoolTest());
}
service.shutdown();
}
}
线程池API
为了方便大家对于线程池的使用,在Executors里面提供了几个线程池的工厂方法,这样,很多新手就不需要了解太多关于ThreadPoolExecutor的知识了,它们只需要直接使用Executors的工厂方法,就可以使用线程池。
Executors提供的工厂方法:
- newFixedThreadPool:该方法返回一个固定数量的线程池,线程数不变,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中,等待有空闲的线程去执行。
- newSingleThreadExecutor:创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。
- newCachedThreadPool:返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在60秒后自动回收。
- newScheduledThreadPool:创建一个可以执行线程的数量的线程池,但是这个线程池还带有延迟和周期性执行任务的功能,类似定时器。
ThreadPoolExecutor
上面提到的四种线程池的构建,都是基于ThreadPoolExecutor来构建的,接下来将一起了解一下面试官最喜欢问道的一道面试题”请简单说下你知道的线程池和ThreadPoolThread有哪些构造参数“。
ThreadPoolExecutor有多个重载的构造方法,我们可以基于它最完整的构造方法来分析,先来解释一下每个参数的作用,稍后我们再分析源码的过程中再来详细了解参数的意义。
public ThreadPoolExecutor(int corePoolSize,// 核心线程数
int maximumPoolSize,// 最大线程数
long keepAliveTime,// 超时时间,超出核心线程数量以外的线程空余存活时间
TimeUnit unit,// 存活时间单位
BlockingQueue<Runnable> workQueue,// 保存执行任务的队列
ThreadFactory threadFactory,// 创建新线程使用的工厂
RejectedExecutionHandler handler)// 当任务无法执行的时候的处理方式
线程池初始化以后做了什么事情?线程池初始化时是没有创建线程的,线程池的线程的初始化与其他线程一样,但是在完成任务以后,该线程不会自行销毁,而是以挂起的状态返回到线程池。直到应用程序再次向线程池发出请求时,线程池里挂起的线程就会再度激活执行任务。这样既节省了建立线程锁造成的性能损耗,也可以让多个任务反复重用同一个线程,从而在应用程序生存期内节约大量开销。
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
FixedThreadPool的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。另外keepAliveTime为0,也就是超出核心线程数量以外的线程空余存活时间。而这里选用的阻塞队列是LinkedBlockingQueue,使用的是默认容量Integer.MAX_VALUE,相当于没有上限。
这个线程池执行任务的流程如下:
- 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务。
- 线程数等于核心线程数后,将任务加入阻塞队里。
- 由于队列容量非常大,可以一直添加
- 执行完任务的线程反复去队列中去任务执行
用途:FixedThreadPool用于负载比较大的服务器,为了资源的合理利用,需要限制当前线程数量。
newCacheThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
CachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。并且没有核心线程,非核心线程数无上限,但是每个空闲的时间只有60秒,超过后就会被回收。
它的执行流程如下:
- 没有核心线程,直接向SynchronousQueue中提交任务。
- 如果有空闲线程,就去取出任务执行。如果没有空闲线程,就新建一个。
- 执行完任务的线程有60秒生存事件,如果在这个时间内可以接到新的任务,就可以继续活下去,否则就被回收。
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行,保证所有任务按照执行顺序(FIFO,LIFO,优先级)执行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
线程池的实现原理分析
线程池的基本使用我们都清楚了,接下来我们了解一下线程池的实现原理。
ThreadPoolExecutor是线程池的核心,提供了线程池的实现。
ScheduledThreadPollExeutor继承了ThreadPoolExecutor,并另外提供一些调度方法以支持定时和周期任务。Executors是工具类,主要用来创建线程池对象。
我们把一个任务提交给线程池去处理的时候,线程池的处理过程是什么样的呢?首先直接来看定义