前面的文章详细的介绍线程相关的内容,但在平时的开发工作中,我们很少去直接创建一个线程使用,一般都是通过线程池的方式来进行调用。这边文章就来介绍一下Java中的线程池是怎么工作的,以及各种线程池之间有什么区别
一、线程与线程池
我们可以通过执行一段相同的代码,来看一下线程和线程池之间的区别
创建多个线程:
Long start = System.currentTimeMillis();
final Random random = new Random();
final List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 100000; i++) {
Thread thread = new Thread() {
@Override
public void run() {
list.add(random.nextInt());
}
};
thread.start();
}
System.out.println("时间:" + (System.currentTimeMillis() - start));
时间:14729
线程池:
Long start = System.currentTimeMillis();
final Random random = new Random();
final List<Integer> list = new ArrayList<Integer>();
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
list.add(random.nextInt());
}
});
}
executorService.shutdown();
System.out.println("时间:"+(System.currentTimeMillis() - start));
时间:21
通过上面两种方式,我们明显的可以看出来,创建多个线程和使用线程池之间有明显的性能差别,造成这种情况的本质原因在于线程对于操作系统来说是一个比较重的资源,它的创建和销毁都需要额外花费CPU很多时间。而线程池可以通过线程复用避免了大量创建线程时的消耗,从而实现高性能的目的
二、线程池创建
Executors类提供这四种创建线程的,但这四种方式我们都不推荐使用,因为使用这种方式创建的线程池,线程池的相关参数都是采用默认的,很容易出现程序OOM的情况,所以我们更推荐使用new关键字来创建线程池,同时手动指定线程池的配置参数
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
我们可以看一下上面这四个方法的源码,就直到如何创建一个线程池了
-
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
-
newCachedThreadPool
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
-
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
-
newScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
从上面这几个这个方法的源码可以看出来,它们内部都是去New一个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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
3.1 corePoolSize
核心线程数(常驻线程数),当线程池添加任务的时候,只要当前线程池中的线程数量没有达到核心线程数,都会创建一个新的线程来执行任务。当所有任务都执行完了之后,如果线程池中的线程数大于核心线程数,那么多出来的这部分线程,就会被销毁,而只保留核心线程数量的线程供后续使用。
3.2 maximumPoolSize
顾名思义,就是线程池中允许同时存在的最大线程数,当任务数量大于核心线程数时,新的任务会先添加到队列中进行等待,当队列也满了的时候,就会去判断当前线程池的线程数是否大于线程池允许的最大线程数,如果小于最大线程数,就会为这些任务创建新的线程,而这些新的线程都是临时线程。
通过Executors创建线程池时,这个值通常被设置为Integer.MAX_VALUE
,当任务量太多事,就会导致OOM
3.3 keepAliveTime&TimeUnit
这个参数指定了临时线程的存活时间,TimeUnit参数指定时间的单位,将传进来的时间转换成纳秒。maximumPoolSize
参数中说明了任务队列满了之后创建临时线程,之所以是临时线程,就是因为当线程池的任务都执行完了之后,这些临时的线程就会被销毁,那么什么时候销毁就是keepAliveTime
参数决定的,超过这个时间后就会被销毁
同时这个参数也可以用于核心线程,如果线程池配置了allowCoreThreadTimeOut
参数为true,表示如果核心线程等待时间超过了keepAliveTime
,也会被回收
3.4 BlockingQueue
阻塞队列,向线程池添加任务时,如果线程池中的线程数已经达到核心线程数,那么新添加进来的任务就会先缓存到阻塞队列中,当某个线程的任务执行完了之后,再从阻塞队列获取任务继续执行。
Executors类中的四种创建线程池的方法分别用到了LinkedBlockingQueue、SynchronousQueue和DelayedWorkQueue
SynchronousQueue是一个同步队列,它并不会存储任务,当线程put一个任务后,该线程就会被阻塞,直到有线程调用take()方法,被阻塞的线程的才会被唤醒,它实现的是线程之间一对一传递消息的模型,而newCachedThreadPool()就是采用这种队列,就会导致一个现象就是,当线程数达到核心线程数后,当有新的任务进来后,就会被阻塞,需要创建一个新的线程来接收刚才的任务,否则就不能再添加任务。
LinkedBlockingQueue是一种链表式的阻塞队列,但它是一个有界队列,我们通过newSingleThreadExecutor()方法创建线程池时,直接new LinkedBlockingQueue<Runnable>()
来创建阻塞队列,但这个阻塞队列默认的容量为Integer.MAX_VALUE
,这也是导致程序出现OOM的原因之一,所以我们在创建阻塞队列的时候,一定要指定其容量。
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
DelayedWorkQueue是一个延时队列,主要用在定时的线程池中,它提供了默认的初始化容量为16,并且可以扩容
private static final int INITIAL_CAPACITY = 16;
private void grow() {
int oldCapacity = queue.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50%
if (newCapacity < 0) // overflow
newCapacity = Integer.MAX_VALUE;
queue = Arrays.copyOf(queue, newCapacity);
}
3.5 ThreadFactory
线程工厂,线程池中都是通过ThreadFactory的newThread()方法创建线程对象,Executors的四种的方法中,都是用DefaultThreadFactory作为线程工厂
3.6 RejectedExecutionHandler
拒接策略,即当任务数填满了阻塞队列,并且当前线程数已经达到线程池规定的最大线程数时,再有新的任务进来,就要采用相应的拒绝策略来处理新的任务。
ThreadPoolExecutor中提供了四种拒绝策略: