ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【Java并发编程系列7】线程池基本知识

【Java并发编程系列7】线程池基本知识 初识线程池我们知道线程的创建和销毁都需要映射到操作系统因此其代价是比较高昂的。出于避免频繁创建、销毁线程以及方便线程管理的需要线程池应运而生。线程池优势降低资源消耗线程池通常会维护一些线程数量为 corePoolSize这些线程被重复使用来执行不同的任务任务完成后不会销毁。在待处理任务量很大的时候通过对线程资源的复用避免了线程的频繁创建与销毁从而降低了系统资源消耗。提高响应速度由于线程池维护了一批 alive 状态的线程当任务到达时不需要再创建线程而是直接由这些线程去执行任务从而减少了任务的等待时间。提高线程的可管理性使用线程池可以对线程进行统一的分配调优和监控。线程池设计思路有句话叫做艺术来源于生活编程语言也是如此很多设计思想能映射到日常生活中比如面向对象思想、封装、继承等等。今天我们要说的线程池它同样可以在现实世界找到对应的实体——工厂。先假想一个工厂的生产流程工厂中有固定的一批工人称为正式工人工厂接收的订单由这些工人去完成。当订单增加正式工人已经忙不过来了工厂会将生产原料暂时堆积在仓库中等有空闲的工人时再处理因为工人空闲了也不会主动处理仓库中的生产任务所以需要调度员实时调度。仓库堆积满了后订单还在增加怎么办工厂只能临时扩招一批工人来应对生产高峰而这批工人高峰结束后是要清退的所以称为临时工。当时临时工也招满后受限于工位限制临时工数量有上限后面的订单只能忍痛拒绝了。我们做如下一番映射工厂——线程池订单——任务Runnable正式工人——核心线程临时工——普通线程仓库——任务队列调度员——getTask()getTask()是一个方法将任务队列中的任务调度给空闲线程。映射后形成线程池流程图如下两者是不是有异曲同工之妙点评一下感觉作者的这个类比太TM经典了直接抓住了线程调用的精髓。这就是为什么看书时不能只盯着书看可能你看半天都不懂书中讲的啥找一篇经典的博客瞬间给你拨开云雾而且还印象深刻。这样线程池的工作原理或者说流程就很好理解了提炼成一个简图深入线程池那么接下来问题来了线程池是具体如何实现这套工作机制的呢从Java线程池Executor框架体系可以看出线程池的真正实现类是ThreadPoolExecutor因此我们接下来重点研究这个类。构造方法研究一个类先从它的构造方法开始。ThreadPoolExecutor提供了4个有参构造方法public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable 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; }感觉构造方法有点多其实前面的基础参数都是一样的就后面两个可选参数“线程工厂”和“拒绝策略”不一样组合一下就是2*24种情况。解释一下构造方法中涉及到的参数corePoolSize必需核心线程数。即池中一直保持存活的线程数即使这些线程处于空闲。但是将allowCoreThreadTimeOut参数设置为true后核心线程处于空闲一段时间以上也会被回收。maximumPoolSize必需池中允许的最大线程数。当核心线程全部繁忙且任务队列打满之后线程池会临时追加线程直到总线程数达到maximumPoolSize这个上限。keepAliveTime必需线程空闲超时时间。当非核心线程处于空闲状态的时间超过这个时间后该线程将被回收。将allowCoreThreadTimeOut参数设置为true后核心线程也会被回收。unit必需keepAliveTime参数的时间单位。有TimeUnit.DAYS天、TimeUnit.HOURS小时、TimeUnit.MINUTES分钟、TimeUnit.SECONDS秒、TimeUnit.MILLISECONDS毫秒、TimeUnit.MICROSECONDS微秒、TimeUnit.NANOSECONDS纳秒workQueue必需任务队列采用阻塞队列实现。当核心线程全部繁忙时后续由execute方法提交的Runnable将存放在任务队列中等待被线程处理。threadFactory可选线程工厂。指定线程池创建线程的方式。handler可选拒绝策略。当线程池中线程数达到maximumPoolSize且workQueue打满时后续提交的任务将被拒绝handler可以指定用什么方式拒绝任务。了解完“线程的设计思路”再看构造方法的这些参数感觉就非常容易懂了。任务队列使用ThreadPoolExecutor需要指定一个实现了BlockingQueue接口的任务等待队列。在ThreadPoolExecutor线程池的API文档中一共推荐了三种等待队列SynchronousQueue同步队列。这是一个内部没有任何容量的阻塞队列任何一次插入操作的元素都要等待相对的删除/读取操作否则进行插入操作的线程就要一直等待反之亦然。LinkedBlockingQueue无界队列严格来说并非无界上限是Integer.MAX_VALUE基于链表结构。使用无界队列后当核心线程都繁忙时后续任务可以无限加入队列因此线程池中线程数不会超过核心线程数。这种队列可以提高线程池吞吐量但代价是牺牲内存空间甚至会导致内存溢出。另外使用它时可以指定容量这样它也就是一种有界队列了。ArrayBlockingQueue有界队列基于数组实现。在线程池初始化时指定队列的容量后续无法再调整。这种有界队列有利于防止资源耗尽但可能更难调整和控制。另外Java还提供了另外4种队列PriorityBlockingQueue支持优先级排序的无界阻塞队列。存放在PriorityBlockingQueue中的元素必须实现Comparable接口这样才能通过实现compareTo()方法进行排序。优先级最高的元素将始终排在队列的头部PriorityBlockingQueue不会保证优先级一样的元素的排序也不保证当前队列中除了优先级最高的元素以外的元素随时处于正确排序的位置。DelayQueue延迟队列。基于二叉堆实现同时具备无界队列、阻塞队列、优先队列的特征。DelayQueue延迟队列中存放的对象必须是实现Delayed接口的类对象。通过执行时延从队列中提取任务时间没到任务取不出来。更多内容请见DelayQueue。LinkedBlockingDeque双端队列。基于链表实现既可以从尾部插入/取出元素还可以从头部插入元素/取出元素。LinkedTransferQueue由链表结构组成的无界阻塞队列。这个队列比较特别的时采用一种预占模式意思就是消费者线程取元素时如果队列不为空则直接取走数据若队列为空那就生成一个节点节点元素为null入队然后消费者线程被等待在这个节点上后面生产者线程入队时发现有一个元素为null的节点生产者线程就不入队了直接就将元素填充到该节点并唤醒该节点等待的线程被唤醒的消费者线程取走元素。感觉这提供的队列有些多啊总共7个说实话我现在还不知道实际场景用哪个比较好后面我们可以看看Java封装好的线程池里面用的队列都是哪种。拒绝策略线程池有一个重要的机制拒绝策略。当线程池workQueue已满且无法再创建新线程池时就要拒绝后续任务了。拒绝策略需要实现RejectedExecutionHandler接口不过Executors框架已经为我们实现了4种拒绝策略AbortPolicy默认丢弃任务并抛出RejectedExecutionException异常。CallerRunsPolicy直接运行这个任务的run方法但并非是由线程池的线程处理而是交由任务的调用线程处理。DiscardPolicy直接丢弃任务不抛出任何异常。DiscardOldestPolicy将当前处于等待队列列头的等待任务强行取出然后再试图将当前被拒绝的任务提交到线程池执行。线程工厂指定创建线程的方式这个参数不是必选项Executors类已经为我们非常贴心地提供了一个默认的线程工厂/** * The default thread factory */ static class DefaultThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber new AtomicInteger(1); private final String namePrefix; DefaultThreadFactory() { SecurityManager s System.getSecurityManager(); group (s ! null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix pool- poolNumber.getAndIncrement() -thread-; } public Thread newThread(Runnable r) { Thread t new Thread(group, r, namePrefix threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() ! Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }线程池状态线程池有5种状态volatile int runState; // runState is stored in the high-order bits private static final int RUNNING -1 COUNT_BITS; private static final int SHUTDOWN 0 COUNT_BITS; private static final int STOP 1 COUNT_BITS; private static final int TIDYING 2 COUNT_BITS; private static final int TERMINATED 3 COUNT_BITS;runState表示当前线程池的状态它是一个 volatile 变量用来保证线程之间的可见性。下面的几个static final变量表示runState可能的几个取值有以下几个状态RUNNING当创建线程池后初始时线程池处于RUNNING状态SHUTDOWN如果调用了shutdown()方法则线程池处于SHUTDOWN状态此时线程池不能够接受新的任务它会等待所有任务执行完毕STOP如果调用了shutdownNow()方法则线程池处于STOP状态此时线程池不能接受新的任务并且会去尝试终止正在执行的任务TERMINATED当线程池处于SHUTDOWN或STOP状态并且所有工作线程已经销毁任务缓存队列已经清空或执行结束后线程池被设置为TERMINATED状态。初始化容量调整关闭线程初始化默认情况下创建线程池之后线程池中是没有线程的需要提交任务之后才会创建线程。在实际中如果需要线程池创建之后立即创建线程可以通过以下两个方法办到prestartCoreThread()boolean prestartCoreThread()初始化一个核心线程prestartAllCoreThreads()int prestartAllCoreThreads()初始化所有核心线程并返回初始化的线程数public boolean prestartCoreThread() { return addIfUnderCorePoolSize(null); //注意传进去的参数是null } public int prestartAllCoreThreads() { int n 0; while (addIfUnderCorePoolSize(null))//注意传进去的参数是null n; return n; }线程池关闭ThreadPoolExecutor提供了两个方法用于线程池的关闭shutdown()不会立即终止线程池而是要等所有任务缓存队列中的任务都执行完后才终止但再也不会接受新的任务shutdownNow()立即终止线程池并尝试打断正在执行的任务并且清空任务缓存队列返回尚未执行的任务线程池容量调整ThreadPoolExecutor提供了动态调整线程池容量大小的方法setCorePoolSize设置核心池大小setMaximumPoolSize设置线程池最大能创建的线程数目大小 当上述参数从小变大时ThreadPoolExecutor进行线程赋值还可能立即创建新的线程来执行任务。使用线程池ThreadPoolExecutor通过构造方法使用ThreadPoolExecutor是线程池最直接的使用方式下面看一个实例public class ThreadPoolExecutorTest { public static void main(String args[]) { // 创建线程池(核心线程数是3最大线程数是5超时时间是5秒) ThreadPoolExecutor threadPool new ThreadPoolExecutor(3,5,5, TimeUnit.SECONDS, new ArrayBlockingQueueRunnable(5)); // 向线程池提交任务 for (int i 0; i threadPool.getCorePoolSize(); i) { threadPool.execute(new Runnable() { Override public void run() { for (int j 0; j 2; j) { System.out.println(Thread.currentThread().getName() : j); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } }); } // 关闭线程池 threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN然后中断所有没有正在执行任务的线程 // threadPool.shutdownNow(); // 设置线程池的状态为STOP然后尝试停止所有的正在执行或暂停任务的线程并返回等待执行任务的列表该方法要慎用容易造成不可控的后果 } } // 输出 // pool-1-thread-1:0 // pool-1-thread-3:0 // pool-1-thread-2:0 // pool-1-thread-2:1 // pool-1-thread-1:1 // pool-1-thread-3:1Executors封装线程池另外Executors封装好了4种常见的功能线程池还是那么地贴心FixedThreadPool固定容量线程池。其特点是最大线程数就是核心线程数意味着线程池只能创建核心线程keepAliveTime为0即线程执行完任务立即回收。任务队列未指定容量代表使用默认值Integer.MAX_VALUE。适用于需要控制并发线程的场景。// 使用默认线程工厂 public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueueRunnable()); } // 需要自定义线程工厂 public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueueRunnable(), threadFactory); }使用示例public class FixedThreadPoolTest { public static void main(String args[]) { // 创建线程池对象设置核心线程和最大线程数为5 ExecutorService fixedThreadPool Executors.newFixedThreadPool(5); fixedThreadPool.execute(new Runnable() { Override public void run() { System.out.println(Thread.currentThread().getName() is running.); try { Thread.sleep(10000L); } catch (InterruptedException e) { e.printStackTrace(); System.out.println(Throw Exception.); } System.out.println(Thread.currentThread().getName() after sleep, is still running.); } }); //fixedThreadPool.shutdown(); fixedThreadPool.shutdownNow(); // 不建议这样使用很危险这里仅用于测试 } } // 输出 // pool-1-thread-1 is running. // Throw Exception. // pool-1-thread-1 after sleep, is still running. // java.lang.InterruptedException: sleep interrupted // at java.lang.Thread.sleep(Native Method) // at com.java.parallel.pool.FixedThreadPoolTest$1.run(FixedThreadPoolTest.java:15) // at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) // at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) // at java.lang.Thread.run(Thread.java:748)作者的这个示例我加了一点料我就想看看直接中断会是什么效果结果发现直接中断后线程直接抛出异常我捕获异常后输出了一些结果。正常情况下捕获异常是需要做一些处理我这里仅作测试。SingleThreadExecutor单线程线程池。特点是线程池中只有一个线程核心线程线程执行完任务立即回收使用有界阻塞队列容量未指定使用默认值Integer.MAX_VALUEpublic static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueueRunnable())); } // 为节省篇幅省略了自定义线程工厂方式的源码使用示例public class SingleThreadExecutorTest { public static void main(String args[]) { // 创建线程池对象设置核心线程和最大线程数为1 ExecutorService singleThreadPool Executors.newSingleThreadExecutor(); singleThreadPool.execute(new Runnable() { Override public void run() { System.out.println(Thread.currentThread().getName() is running.); } }); singleThreadPool.shutdown(); } } // 输出 // pool-1-thread-1 is running.ScheduledThreadPool定时线程池。指定核心线程数量普通线程数量无限线程执行完任务立即回收任务队列为延时阻塞队列。这是一个比较特别的线程池适用于执行定时或周期性的任务。public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } // 继承了 ThreadPoolExecutor public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService { // 构造函数省略了自定义线程工厂的构造函数 public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); } // 延时执行任务 public ScheduledFuture? schedule(Runnable command, long delay, TimeUnit unit) { ... } // 定时执行任务 public ScheduledFuture? scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {...} }使用示例public class ScheduledThreadPoolTest { public static void main(String args[]) { // 创建定时线程池 ScheduledExecutorService scheduledThreadPool Executors.newScheduledThreadPool(5); // 向线程池提交任务 scheduledThreadPool.schedule(new Runnable(){ public void run() { System.out.println(Thread.currentThread().getName() ---运行); } }, 5, TimeUnit.SECONDS); // 延迟5s后执行任务 scheduledThreadPool.shutdown(); } } // 输出 // pool-1-thread-1---运行CachedThreadPool缓存线程池。没有核心线程普通线程数量为Integer.MAX_VALUE可以理解为无限线程闲置60s后回收任务队列使用SynchronousQueue这种无容量的同步队列。适用于任务量大但耗时低的场景。public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueueRunnable()); }使用示例public class CachedThreadPoolTest { public static void main(String args[]) { ExecutorService cachedThreadPool Executors.newCachedThreadPool(); cachedThreadPool.execute(new Runnable(){ public void run() { System.out.println(Thread.currentThread().getName() ---运行); } }); cachedThreadPool.shutdown(); } } // 输出 // pool-1-thread-1---运行最后总结一下Executors封装线程池每种方式用的是哪种队列固定容量线程池FixedThreadPoolLinkedBlockingQueue无界队列单线程线程池SingleThreadExecutorLinkedBlockingQueue无界队列定时线程池ScheduledThreadPoolDelayQueue延迟队列缓存线程池CachedThreadPoolSynchronousQueue同步队列稍微解读一下FixedThreadPool构造函数的keepAliveTime0然后核心线程个数和最大线程个数都限制死了所以虽然用的是LinkedBlockingQueue无界队列但是其实不会用到无界的特性其实是当有界来用。SingleThreadExecutor核心线程和最大线程都是1keepAliveTime0这个也是拿LinkedBlockingQueue无界队列当有界队列使用。ScheduledThreadPool因为需要延时这个使用DelayQueue延迟队列就没有任何毛病。CachedThreadPool没有核心线程普通线程设置成最大值用的SynchronousQueue没有任何容量这个不太理解后面再研究一下。
返回列表