JDK默认提供了四种线程池:SingleThreadExecutor、FiexdThreadPool、CachedThreadPool、ScheduledThreadPoolExecutor。
本文会先从前三个线程池的使用开始讲解,然后过度到线程池参数、拒绝策略等方面进行全面讲解,最后自己根据参数构造一个
线程池。
SingleThreadExecutor
public static void singleThreadExecutorTest() { ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.execute(()->{ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); executorService.execute(()->{ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); executorService.execute(()->{ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); try { Thread.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } executorService.shutdown(); }
该代码是使用Exectors工具类创建的一个大小为1的线程池,并且创建三个任务提交到该线程执行,每个任务执行需要一秒。因此从运行结果中我们可以很清楚的看到三个任务一次只能执行一个线程。
FiexdThreadPool
public static void fixedThreadPoolTest() { ExecutorService service = Executors.newFixedThreadPool(2); service.execute(()->{ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); service.execute(()->{ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); service.execute(()->{ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } service.shutdown(); }
该代码是使用Exectors工具类创建的一个大小为2的线程池,并且创建三个任务提交到该线程执行,每个任务执行需要一秒。从结果可以看到第一秒两个线程一起执行了,第二秒第三个线程才执行。
CachedThreadPool
public static void cachedThreadPoolTest() { ExecutorService service = Executors.newCachedThreadPool(); service.execute(()->{ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); service.execute(()->{ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); service.execute(()->{ System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } service.shutdown(); }
该代码是使用Exectors工具类创建的一个大小为Integer.MAX_VALUE的线程池,并且创建三个任务提交到该线程执行,每个任务执行需要一秒。从结果可以看到三个线程一起执行了。
原理分析
构造SingThreadExecutor需要的参数
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
构造FiexdThreadPool需要的参数
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
构造CachedThreadPool需要的参数
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
通过上面三个线程池构造的源码我们可以看到,创建线程池的时候都是通过创建ThreadPoolExecutor对象,创建ThreadPoolExecutor需要5个参数,那么这5个参数是什么呢,我们继续跟踪源码
构造ThreadPoolExecutor对象需要的参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
这里可以看到生成ThreadPoolExecutor对象需要为其构造参数传递5个参数,但是下面的this里却是7个参数。原来有两个参数是已经固定好的。我们跟踪进去看看这七个参数
ThreadPoolExecutor构造器源码
到这里重头戏来了,下面这个代码才是真真正在创建ThreadPoolExecutor对象的构造器源码。我们可以看到一共有7个参数,我们来谈谈这七个参数
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; }
创建线程池需要的七个参数
int corePoolSize : 常驻核心线程数
int maximumPoolSize:最大线程数
long keepAliveTime:除了常驻线程的额外线程的存活时间
TimeUnit unit:存活时间的单位
BlockingQueue<Runnable> workQueue:任务队列
ThreadFactory threadFactory:创建线程的工厂
RejectedExecutionHandler handler:拒绝策略
我们先眼熟一下这七个参数,接下来我们从线程池的工作流程来讲解这七个参数
线程池工作流程
线程池刚创建的时候里面是空的,这时候还没有线程。当main线程往线程池里面加任务的时候才会开始创线程
1 主线程往线程池里添加任务,发现核心线程为空,创建核心线程执行任务
2 随着主线程往线程池里加任务,核心线程都被占用了,这时任务会被放入任务队列
3 随着主线程继续往线程池里加任务,任务队列也满了,线程池会创建额外线程执行任务。
4 主线程继续添加任务,任务队列满了,核心线程满了。额外线程+核心线程=总线程数,即额外线程也满了,这时候就会执行拒绝策略。
5 如果没有到4那么极端,随着任务的执行,任务队列越来越少,直至没有,那么额外线程在等待keepaliveTime时间后被销毁,线程池里只剩下核心线程存在。
相信知道了流程以后,上面的7个参数我们也就知道都是什么了,这里需要注意的一点是核心线程数是包含在总线程数里面。
谈谈JDK自带的线程池弊端
从上面我们可以看到,JDK自带的SingleThreadExecutor、FixedThreadPool都是采用LinkedBlockingQueue阻塞队列作为任务队列,由于LinkedBlockingQueue是一个
大小为Integer.MAX_VALUE的阻塞队列,因此main线程在添加任务的时候阻塞队列不会满,也就是不会触发拒绝策略,可能会导致任务持续添加引发OOM。而CachedThreadPool
的最大线程数为Integer.MAX_VALUE,会无线创建线程,来一个任务创一个线程,可能会导致线程创太多导致OOM所有我们一般都是自己构造参数创线程池。
四大拒绝策略
AbortPolicy:当任务队列和线程达到最大线程数后还添加任务的话会直接抛异常,阻止程序运行
CallerRunsPolicy:当任务队列和线程达到最大线程数后还添加任务不会抛异常和抛弃任务,而是会将任务回退给调用者。
DiscardOldestPolicy:当任务队列和线程达到最大线程数后还添加任务的话会将任务队列中等待最久的一个任务抛弃掉然后添加新任务
DiscardPolicy:直接抛弃任务。不做任何处理也不抛异常
自己构造线程池
该线程池核心线程数为2,最大线程数为5,额外线程存活时间为60秒,阻塞队列大小为3,采用默认的线程工厂,采用默认的拒绝策略,即抛异常
因此该线程池最多可以连续提供8个任务,超过8个很可能促发拒绝策略,抛异常
public static void myThreadPool() { ExecutorService threadPool = new ThreadPoolExecutor(2, 5, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); for(int i=0;i<8;i++) { threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()); }); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } threadPool.shutdown(); }
线程池参数如何选择
CPU密集型:CPU密集型CPU使用率高,一直在工作,空闲时间少,因此最大线程数最好设为CPU核数+1
IO密集型:IO密集型大部分时间都在做IO操作,CPU空闲时间多,因此最大线程数最好设为CPU核数*2.