备案网站容易被收录,wordpress必要的插件,WordPress知更鸟lts,东莞seo计费线程池
1. 初识线程池
我们之所以引入线程#xff0c;是因为进程的创建和销毁过于重量#xff0c;而线程可以共享更多内存资源#xff0c;因此成为显著提高效率的手段。但线程也是 OS 分配的#xff0c;也涉及用户态和内核态的切换#xff0c;也是一种很有限的资源是因为进程的创建和销毁过于重量而线程可以共享更多内存资源因此成为显著提高效率的手段。但线程也是 OS 分配的也涉及用户态和内核态的切换也是一种很有限的资源其创建和销毁的开销虽然比进程小但也不可忽视。 如果想在线程的基础上进一步提高并发编程的效率目前有两种主流解决方案 协程这是纯用户态的轻量级线程由程序自己控制调度不涉及操作系统内核切换。在 Java 中被称为 VirtualThread。 线程池使用池化技术提高资源利用率。 Java 的生态中更普遍使用线程池这也是下面要着重介绍的。2. 优势 **1. 提高资源利用率**线程是稀缺资源使用线程池可以重复利用已创建的线程减少了许多用户态与内核态之间相互转换的过程从池中取线程是用户态的操作而创建线程需要内核切换由此来降低创建销毁线程的消耗。 **2. 提高响应速度**任务无需等待线程创建就可以立即执行。 **3. 提高线程管理性**线程池允许我们进行很多配置通过这些配置来控制线程的分配监控线程的运行状态。3. ThreadPoolExecutor ThreadPoolExecutor 是 Java 线程池最重要的构造类。3.1 提交任务 使用void excute(Runnable command)或T FutureT submit(CallableT task)向线程池提交任务。前者无法接收任务返回值后者可以接收任务返回值也可以进行异常捕获。publicTFutureTsubmit(CallableTtask){if(tasknull)thrownewNullPointerException();// 将任务包装为 FutureTaskRunnableFutureTftasknewTaskFor(task);// submit 最终调用的还是 excuteexecute(ftask);// 返回 FutureTask 引用returnftask;}// 创建 FutureTaskprotectedTRunnableFutureTnewTaskFor(CallableTcallable){returnnewFutureTaskT(callable);} 使用实例publicclassSubmitExample{publicstaticvoidmain(String[]args)throwsException{ExecutorServiceexecutorExecutors.newFixedThreadPool(3);ListFutureStringfuturesnewArrayList();for(inti0;i5;i){finalinttaskIdi;FutureStringfutureexecutor.submit(()-{Thread.sleep(1000);returnResult from task taskId;});futures.add(future);}// 获取结果try{for(FutureStringfuture:futures){// 每个 get() 调用都会等待任务完成System.out.println(future.get());}}catch(InterruptedExceptione){Thread.currentThread().interrupt();break;}executor.shutdown();}} 除了这两种最基本的方式之外我们也可以基于CompletableFuture类进行任务提交和编排。CompletableFuture是 Java 8 提供的功能非常强大的异步编程类后面会单独出专题来讲这个类。3.2 线程池状态与优雅关闭 调用shutdown()后线程池将为空闲线程设置中断信号线程池不再接受新任务会直接采用拒绝策略但会将缓冲区中的剩余任务执行完毕。 调用shutdownNow()后线程池将为所有线程设置中断信号线程池不再接受新任务会直接采用拒绝策略然后直接将任务队列中的任务转移到一个 List 中返回给调用方。注意在 Java 中线程的中断始终是协作式的也就是说即使调用shutdownNow()也不意味着对线程池的关闭可以很快成功这还取决于任务是否能够对中断信号做出及时响应。如果任务一直在进行逻辑运算却不检查Thread.interrupted()状态那么线程还是会继续执行完整个任务后才释放。因此我们的任务在原则上应该有响应中断信号的能力。 调用shutdown()或shutdownNow()的线程并不会等待线程池关闭完成也就是说这两个方法都是立即返回的如果要等待线程池关闭完成需主动调用awaitTermination(long timeout, TimeUnit unit)。 优雅关闭线程池的示例代码如下publicvoidgracefulShutdown(ExecutorServiceexecutor,longtimeout,TimeUnitunit)throwsInterruptedException{// 阻止新任务提交executor.shutdown();try{// 等待现有任务完成if(!executor.awaitTermination(timeout,unit)){// 如果超时尝试取消正在执行的任务executor.shutdownNow();// 再次等待给任务响应中断的机会if(!executor.awaitTermination(timeout,unit)){System.err.println(线程池未完全终止);}}}catch(InterruptedExceptione){// 如果当前线程被中断也尝试立即关闭executor.shutdownNow();// 恢复中断状态Thread.currentThread().interrupt();}}3.3 构造器publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueueRunnableworkQueue,ThreadFactorythreadFactory,RejectedExecutionHandlerhandler)参数解释corePoolSize核心线程数一般情况下核心线程会一直存活不会被超时回收。maximumPoolSize最大线程数线程池的最大容纳线程数即核心线程数 非核心线程数。如果工作队列已满才会创建非核心线程因此当使用无界队列时该参数将失效因为任务会一直堆积在队列中永远不会触发创建超出核心线程数的线程。keepAliveTime线程回收一般情况下只针对非核心线程如果在这个时长内没有执行任何任务则会被回收。因此一般情况下线程池中的线程数目会在 [ corePoolSizemaximumPoolSize ] 的范围内动态变化。unit指定 keepAliveTime 的时间单位。比如TimeUnit.MILLISECONDS毫秒、TimeUnit.SECONDS秒、TimeUnit.MINUTES分等。workQueue任务队列如果核心线程均在执行任务那么此时继续添加的任务会存放到任务队列中。PriorityBlockingQueue无界阻塞队列。在构造函数中指定的容量只是初始化大小但该队列始终是无界的。适合任务有优先级的场景。ArrayBlockingQueue有界阻塞队列。强制指定容量入队和出队使用同一把锁因此吞吐量略低于 LinkedBlockingQueue。LinkedBlockingQueue非强制有界阻塞队列。如果构造时不传容量则相当于无界队列。入队和出队使用不同的锁。SynchronousQueue零容量的阻塞队列。生产者投递任务后不会返回而是会等待消费者来取走这个元素消费者在取任务时如果没有生产者在等待它也会被阻塞直到有生产者提供数据。因此它的行为类似于 “移交”直接将任务从生产者交给消费者。在这种队列下线程池的 maximumPoolSize 通常被设置为无限大适合一些快问快答的场景。threadFactory选填线程工厂使所有线程都可以在创建时获得统一的处理从而为池中的所有线程提供统一的配置。如果不填则会使用 Executors 类中默认的线程工厂实现该实现会为每个线程设置一个线程组、一个名称前缀并确保线程不是守护线程且具有正常的优先级。如果想自己命名和决定是否为守护线程可以实现 ThreadFactory 接口。handler选填拒绝策略如果线程池在以最大线程数运行继续添加任务就会触发拒绝策略。ThreadPoolExecutor 已经为我们搭好了四种常用的拒绝策略框架作为内部类存在于 ThreadPoolExecutor 中如下AbortPolicy默认放弃任务并抛出异常。在关键业务中使用可以在系统无法承受并发量时及时发现。DiscardPolicy直接放弃任务相当于 AbortPolicy 的静默版本适合一些不太重要的业务。CallerRunsPolicy由添加任务的线程生产者负责执行该任务。在不想放弃任务的时候使用此时生产者在帮助消费不能继续生产添加任务。在极端情况下有可能生产者消费这一任务需要花费很长时间而在这个过程中线程池已经将任务队列中的任务全部消费完了接下来线程池就会陷入 “饥饿”。DiscardOldestPolicy喜新厌旧即从队列中把队头挤出去重新尝试添加该任务到队尾。如果想要自定义拒绝策略可以实现 RejectedExecutionHandler 接口。3.4 钩子方法 通过继承 ThreadPoolExecutor 在子类中重写三个钩子方法示例如下publicclassCustomThreadPoolextendsThreadPoolExecutor{// 起始时间注册为 ThreadLocal每个线程独立维护privatefinalThreadLocalLongstartTimenewThreadLocal();// 计算任务总耗时privatefinalAtomicLongtotalTimenewAtomicLong(0);// 计算任务总数privatefinalAtomicLongtaskCountnewAtomicLong(0);publicCustomThreadPool(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueueRunnableworkQueue){super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue);}// 任务执行前钩子OverrideprotectedvoidbeforeExecute(Threadt,Runnabler){super.beforeExecute(t,r);startTime.set(System.currentTimeMillis());// 设置 ThreadLocalSystem.out.println(String.format(线程[%s]开始执行任务,t.getName()));}// 任务执行后钩子OverrideprotectedvoidafterExecute(Runnabler,Throwablet){super.afterExecute(r,t);longendTimeSystem.currentTimeMillis();longcostendTime-startTime.get();taskCount.incrementAndGet();totalTime.addAndGet(cost);System.out.println(String.format(任务执行耗时: %dms,cost));if(t!null){// 进行统一异常处理、记录日志、发送报警等}startTime.remove();// 清理 ThreadLocal}// 线程池终止前钩子Overrideprotectedvoidterminated(){super.terminated();System.out.println(总任务数: taskCount.get());System.out.println(总耗时: totalTime.get()ms);}}3.5 执行流程初始化初始时线程池内部的工作线程数为 0。只有当新任务提交时才会按需创建线程懒加载。当任务提交到线程池时根据当前线程数判断当前线程数 corePoolSize创建新线程执行任务即使有其他空闲线程。当前线程数 corePoolSize将任务放入队列等待当有空闲线程时取出执行。队列已满且线程数 maximumPoolSize创建新线程执行任务。队列已满且线程数 maximumPoolSize触发拒绝策略。当线程空闲时间超过keepAliveTime多余的线程会被回收直到线程数不超过corePoolSize。 有一道经典面试题就是在考察对这个流程的熟悉程度设一个线程池的核心线程数为 10最大线程数为 20阻塞队列容量 30提交 45 个任务每个耗时 500 ms问这批任务共需多少时间执行完毕只考虑任务本身耗时。 第一个 500 ms10 个任务被核心线程消费30 个入队还有 5 个被非核心线程消费。 第二个 500 ms现在池中有 15 个线程消费缓冲区中的一半任务。 第三个 500 ms消费后一半任务。4. Executors 觉得初始化线程池要传那么多参数很麻烦其实 Executors 这个工厂类已经为我们封装好了四种常用的线程池供我们使用。但是这四种线程池都有一些短板所以还是更推荐使用上面着重介绍的构造方法。虽然自己传参数有些麻烦但是可以让我们更加谨慎地考虑这些参数对业务、系统资源的影响。 这些都是通过 Executors 中的工厂方法构造出来的 FixedThreadPool 和 SingleThreadExecutor 的缺点永远不会拒绝任务除非内存溢出因为它们都使用无界队列。 CachedThreadPool 的缺点它使用 SynchronousQueue 直接移交任务并且消费线程可以被无限多地创建这意味着不会发生任何阻塞但线程创建太多也很容易内存溢出。5. 实践如何确定核心线程数 对于 IO 密集型任务其 CPU 利用率较低一个线程可能大多数时间都在等待因此可以将线程数量设置为较大的值。即使一些线程在阻塞等待也可以启用其他线程继续使用 CPU。通常这个值可以设置为 CPU 核心数的两倍。 对于 CPU 密集型任务其 CPU 利用率较高因此线程数等于 CPU 核心数的时候就已经能充分利用其资源了如果再多启用线程导致更多任务并发执行反而会因为线程之间的切换调度而减低效率。实际上我们使用多线程的初心就是提高 CPU 利用率如果任务的 CPU 利用率本身已经很高其实根本不需要多线程比如 Redis。 对于混合型任务其实没有一个铁律去告诉我们该如何设置线程数正确做法是使用实验的方式对程序进行性能测试观察设置各种线程数目时的情况找出最符合预期的那个这也是很多动态线程池组件所出现的意义。两个典型的使用线程池获取并发性的场景 并行执行子任务比如用户要查看一个商品的信息那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来展示给用户。这种场景最重要的就是获取最大的响应速度去满足用户所以不应该设置队列去缓冲任务应使用 SynchronousQueue调高 corePoolSize 和 maxPoolSize 去尽可能创造多的线程快速执行任务。这个场景可以理解为 IO 密集型。 并行执行大量计算任务比如统计报表用于后续营销策略的分析。这种场景并不要求响应速度只要充分利用已有的硬件资源就可以了。因此可以使用队列去缓冲任务队列容量必须声明防止任务无限制堆积。线程数不宜设置过多会导致上下文切换频繁。这个场景可以理解为 CPU 密集型。6. 必读Java线程池实现原理及其在美团业务中的实践 - 美团技术团队硬核干货4W字从源码上分析JUC线程池ThreadPoolExecutor的实现原理 - throwable - 博客园Java 线程池详解 | JavaGuide