当前位置: 首页 > 科技观察

7种创建线程池的方法,强烈推荐你使用...

时间:2023-03-13 22:49:38 科技观察

根据摩尔定律:集成电路上可容纳的晶体管数量每18个月翻一番,所以集成电路上的晶体管数量CPU会增加更多。但随着时间的推移,一块集成电路所能容纳的晶体管数量已经趋于饱和,摩尔定律也逐渐失效。因此,多核CPU逐渐成为主流,相应的多线程编程也开始流行和普及。当然,这是很久以前的事了。就目前而言,多线程编程已经成为程序员必备的职业技能。那么我们来看看多线程编程中最重要的话题“线程池”。什么是线程池?线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它预先将多个线程存储在一个“池”中。当任务发生时,可以避免重新创建和销毁线程带来的性能开销。它只需要从“池”中取出相应的线程,就可以执行相应的任务。.池化的思想在计算机中也被广泛应用,例如:MemoryPooling:预先申请内存,提高内存申请速度,减少内存碎片。连接池:预先申请数据库连接,提高申请连接的速度,降低系统开销。对象池:回收对象以减少初始化和释放过程中代价高昂的资源损失。线程池的优势主要体现在以下四点:降低资源消耗:创建的线程通过池化技术得到复用,减少线程创建和销毁带来的损失。改进的响应能力:当任务到达时,它会立即执行,而无需等待线程创建。提高线程的可管理性:线程是稀缺资源。如果无限制地创建它们,不仅会消耗系统资源,还会因线程分配不合理导致资源调度不平衡,降低系统稳定性。使用线程池进行统一分配、调优和监控。提供更多更强大的功能:线程池是可扩展的,允许开发者为其添加更多的功能。例如延迟定时线程池ScheduledThreadPoolExecutor,可以让任务延迟或周期性执行。同时,阿里巴巴也在其《Java开发手册》中规定:线程资源必须通过线程池提供,不允许在应用程序中显式创建线程。说明:线程池的好处是减少创建和销毁线程所花费的时间和系统资源的开销,解决资源不足的问题。如果不使用线程池,可能会导致系统创建大量相同类型的线程,导致内存消耗或“过度切换”。了解了线程池是什么以及为什么要使用线程池之后,我们来看看如何使用线程池。使用线程池创建线程池的方式一共有7种,但是大体上可以分为2类:一类是通过ThreadPoolExecutor创建的线程池;另一个是通过Executors创建的线程池。创建线程池一共有7种方式(其中6种由Executors创建,1种由ThreadPoolExecutor创建):Executors.newFixedThreadPool:创建一个固定大小的线程池,可以控制并发线程数。线程将在队列中等待;Executors.newCachedThreadPool:创建一个可缓存的线程池。如果线程数超过处理要求,缓存会在一段时间后回收。如果线程数不够,则创建一个新线程;Executors.newSingleThreadExecutor:创建单线程Executors.newScheduledThreadPool:创建可以执行延迟任务的线程池;Executors.newSingleThreadScheduledExecutor:创建一个可以执行延迟任务的单线程线程池;Executors.newWorkStealingPool:创建线程池,用于抢占式执行(任务执行顺序不确定)【JDK1.8新增】。ThreadPoolExecutor:最原始的线程池创建方式,包含7个参数设置,后面会详细介绍。单线程池的含义从上面的代码我们可以看出newSingleThreadExecutor和newSingleThreadScheduledExecutor都创建了单线程池,那么单线程池是什么意思呢?答:虽然是单线程池,但是提供了工作队列和生命周期管理。工作线程维护等功能。然后我们来看一下每个线程池创建的具体使用。1.FixedThreadPool创建一个固定大小的线程池,可以控制并发线程数,超出的线程会在队列中等待。使用示例如下:publicstaticvoidfixedThreadPool(){//创建2个数据级线程池ExecutorServicethreadPool=Executors.newFixedThreadPool(2);//创建任务Runnablerunnable=newRunnable(){@Overridepublicvoidrun(){System.out.println("taskExecuted,thread:"+Thread.currentThread().getName());}};//线程池执行任务(一次添加4个任务)//执行任务有两种方式:submitandexecutethreadPool.submit(runnable);//执行方式一:submitthreadPool.execute(runnable);//执行方式二:executethreadPool.execute(runnable);threadPool.execute(runnable);}执行结果如下:如果觉得上面的方法比较麻烦,也可以使用更简单的使用方法如下代码所示:publicstaticvoidfixedThreadPool(){//创建线程池ExecutorServicethreadPool=Executors.newFixedThreadPool(2);//执行任务threadPool.execute(()->{System.out.println("TaskExecuted,thread:"+Thread.currentThread().getName());});}2.CachedThreadPool创建一个可缓存的线程池。如果线程数超过处理要求,缓存会在一段时间后回收。如果线程数不够,就新建一个线程。使用示例如下:publicstaticvoidcachedThreadPool(){//创建线程池ExecutorServicethreadPool=Executors.newCachedThreadPool();//执行任务for(inti=0;i<10;i++){threadPool.execute(()->{System.out.println("任务执行完毕,thread:"+Thread.currentThread().getName());try{TimeUnit.SECONDS.sleep(1);}catch(InterruptedExceptione){}});}}执行结果如下:从上面的结果可以看出,线程池创建了10个线程来执行相应的任务。3.SingleThreadExecutor创建一个线程数单一的线程池,可以保证先进先出的执行顺序。使用示例如下:publicstaticvoidsingleThreadExecutor(){//创建线程池ExecutorServicethreadPool=Executors.newSingleThreadExecutor();//执行任务for(inti=0;i<10;i++){finalintindex=i;threadPool.execute(()->{System.out.println(index+":taskisexecuted");try{TimeUnit.SECONDS.sleep(1);}catch(InterruptedExceptione){}});}}执行结果如下如下:4.ScheduledThreadPool为延迟任务创建可执行线程池。使用示例如下:publicstaticvoidscheduledThreadPool(){//创建线程池ScheduledExecutorServicethreadPool=Executors.newScheduledThreadPool(5);//添加定时执行任务(1s后执行)System.out.println("添加任务,time:"+newDate());threadPool.schedule(()->{System.out.println("任务执行完毕,时间:"+newDate());try{TimeUnit.SECONDS.sleep(1);}catch(InterruptedExceptione){}},1,TimeUnit.SECONDS);}执行结果如下:从上面的结果我们可以看出任务是在1秒后执行的,符合我们的预期。5.SingleThreadScheduledExecutor创建一个单线程线程池,可以执行延时任务。使用示例如下:publicstaticvoidSingleThreadScheduledExecutor(){//创建线程池ScheduledExecutorServicethreadPool=Executors.newSingleThreadScheduledExecutor();//添加定时执行任务(2s后执行)System.out.println("添加任务,时间:"+newDate());threadPool.schedule(()->{System.out.println("任务执行完毕,时间:"+newDate());try{TimeUnit.SECONDS.sleep(1);}catch(InterruptedExceptione){}},2,TimeUnit.SECONDS);}执行结果如下:从上面的结果我们可以看出任务是在2秒后执行的,符合我们的预期。6、newWorkStealingPool创建一个线程池,用于抢占式执行(任务执行顺序不定)。注意这个方法只能在JDK1.8+版本中使用。使用示例如下:publicstaticvoidworkStealingPool(){//创建线程池ExecutorServicethreadPool=Executors.newWorkStealingPool();//执行任务for(inti=0;i<10;i++){finalintindex=i;threadPool.execute(()->{System.out.println(index+"isexecuted,threadname:"+Thread.currentThread().getName());});}//确保任务执行完成while(!threadPool.isTerminated()){}}执行结果如下:从上面的结果可以看出任务的执行顺序是不确定的,因为它是抢占式执行的。7.ThreadPoolExecutor是最原始的线程池创建方式,包含7个参数设置。使用示例如下:publicstaticvoidmyThreadPoolExecutor(){//创建线程池ThreadPoolExecutorthreadPool=newThreadPoolExecutor(5,10,100,TimeUnit.SECONDS,newLinkedBlockingQueue<>(10));//执行任务for(inti=0;i<10;i++){finalintindex=i;threadPool.execute(()->{System.out.println(index+"执行,线程名:"+Thread.currentThread().getName());try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}});}}执行结果如下:ThreadPoolExecutor参数介绍ThreadPoolExecutor最多可以设置7个参数,如下代码所示:publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueueworkQueue,ThreadFactorythreadFactory,RejectedExecutionHandlerhandler){//省略...}七个参数含义如下:参数一:corePoolSize核心线程数,是线程数永远活在线程池中。参数2:maximumPoolSize最大线程数,线程池允许的最大线程数,当线程池的任务队列满时可以创建的最大线程数。参数3:keepAliveTime最大线程数可以存活的时间。当线程中没有任务执行时,最大线程会销毁一部分,最后保持核心线程数。参数4:unit:单位与参数3的生存时间配合使用,一起使用来设置线程的生存时间。参数keepAliveTime的时间单位有以下7种选择:TimeUnit.DAYS:天TimeUnit.HOURS:小时TimeUnit.MINUTES:分钟TimeUnit.SECONDS:秒TimeUnit.MILLISECONDS:毫秒TimeUnit.MICROSECONDS:细微TimeUnit.NANOSECONDS:纳秒参数5:workQueue一个阻塞队列,用于存放等待线程池执行的任务,都是线程安全的,它包含以下7种类型:ArrayBlockingQueue:由数组结构组成的有界阻塞队列。LinkedBlockingQueue:由链表结构组成的有界阻塞队列。SynchronousQueue:不存储元素的阻塞队列,即不持有元素直接提交给线程。PriorityBlockingQueue:无界阻塞队列,支持优先级排序。DelayQueue:使用优先级队列实现的无界阻塞队列,只有在延迟到期时才能从中提取元素。LinkedTransferQueue:由链表结构组成的无界阻塞队列。类似于SynchronousQueue,也包含非阻塞方法。LinkedBlockingDeque:由链表结构组成的双向阻塞队列。比较常用的有LinkedBlockingQueue和Synchronous,线程池的排队策略和BlockingQueue有关。参数6:threadFactory线程工厂,主要用于创建线程,默认为普通优先级,非守护线程。参数7:Handlerrejectionpolicy,拒绝处理任务时的策略,系统提供4个选项:AbortPolicy:拒绝并抛出异常。CallerRunsPolicy:使用当前调用线程来执行这个任务。DiscardOldestPolicy:丢弃队列头部(最旧)的任务并执行当前任务。DiscardPolicy:忽略并丢弃当前任务。默认策略是AbortPolicy。线程池的执行流程ThreadPoolExecutor的关键节点执行流程如下:当线程数小于核心线程数时,创建一个线程。当线程数大于等于核心线程数且任务队列未满时,将任务放入任务队列。当线程数大于等于核心线程数,且任务队列已满时:若线程数小于最大线程数,则创建线程;如果线程数等于最大线程数,则抛出异常并拒绝任务。线程池的执行流程如下图所示:Threadrejectionpolicy我们来演示一下ThreadPoolExecutor的拒绝策略的触发。我们使用了DiscardPolicy的拒绝策略,它会忽略并丢弃当前任务的策略。实现代码如下:publicstaticvoidmain(String[]args){//任务的具体方法Runnablerunnable=newRunnable(){@Overridepublicvoidrun(){System.out.println("当前任务执行完毕,执行时间:"+newDate()+"执行线程:"+Thread.currentThread().getName());try{//等待1sTimeUnit.SECONDS.sleep(1);}catch(InterruptedExceptione){e.printStackTrace();}}};//创建线程,线程的任务队列长度为1ThreadPoolExecutorthreadPool=newThreadPoolExecutor(1,1,100,TimeUnit.SECONDS,newLinkedBlockingQueue<>(1),newThreadPoolExecutor.DiscardPolicy());//添加并执行4个任务threadPool.execute(runnable);threadPool.execute(runnable);threadPool。execute(runnable);threadPool.execute(runnable);}我们创建了一个线程池,核心线程数和最大线程数为1,并将线程池的任务队列设置为1,这样当我们有morethan2任务执行时会触发拒绝策略,执行结果如下图所示:从上面的结果可以看出只有两个任务被正确执行,其他多余的任务被丢弃和忽略。其他拒绝策略的使用类似,这里不再赘述。自定义拒绝策略除了Java本身提供的4种拒绝策略外,我们还可以自定义拒绝策略。示例代码如下:publicstaticvoidmain(String[]args){//任务的具体方法Runnablerunnable=newRunnable(){@Overridepublicvoidrun(){System.out.println("当前任务执行完毕,执行时间:"+newDate()+"执行线程:"+Thread.currentThread().getName());尝试{//等待1sTimeUnit.SECONDS。sleep(1);}catch(InterruptedExceptione){e.printStackTrace();}}};//创建线程,线程的任务队列长度为1ThreadPoolExecutorthreadPool=newThreadPoolExecutor(1,1,100,TimeUnit.SECONDS,newLinkedBlockingQueue<>(1),newRejectedExecutionHandler(){@OverridepublicvoidrejectedExecution(Runnabler,ThreadPoolExecutorexecutor){//执行自定义拒绝策略的相关操作System.out.println("我是自定义拒绝策略~");}});//添加并执行4个任务threadPool.execute(runnable);threadPool.execute(runnable);threadPool.execute(runnable);threadPool.execute(runnable);}程序执行结果如下:选择哪个线程池?经过上面的学习我们对整个线程池有了一定的了解,那么如何选择线程池呢?来看看阿里巴巴给我们的回答《Java开发手册》:【强制】不允许使用Executors来创建线程池,而是通过ThreadPoolExecutor这样处理,让作者更清楚线程池的运行规则线程池,避免资源耗尽的风险。注意:Executors返回的线程池对象的缺点如下:1)FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能累积大量请求,导致OOM。2)CachedThreadPool:允许创建的线程数为Integer.MAX_VALUE,可能会创建大量线程,导致OOM。所以综上所述,我们推荐使用ThreadPoolExecutor方式来创建线程池,因为这种创建方式的可控性更强,明确了线程池的运行规则,可以规避一些未知的风险。小结在本文中,我们介绍了创建线程池的7种方式。其中ThreadPoolExecutor是最值得推荐的线程池创建方式。ThreadPoolExecutor最多可以设置7个参数。当然设置5个参数也可以正常使用。ThreadPoolExecutor提供了4种拒绝策略,当数量过多(无法处理)时,当然我们也可以自定义拒绝策略,希望本文的内容可以帮助到你。原创不易,觉得不错就点个赞走吧!参考&感谢https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.htmlhttps://www.cnblogs。com/pcheng/p/13540619.html