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

为小白写的线程池,你学会了吗?

时间:2023-03-12 21:33:20 科技观察

为什么要用线程池?下面是一段创建线程并运行的代码:for(inti=0;i<100;i++){newThread(()->{System.out.println("runthread->"+Thread.currentThread().getName());userService.updateUser(...);}).start();}我们想用这个方法来做异步,或者提高性能,然后把一些消费操作放到一个要运行的新线程。这种思路没问题,但是这段代码就有问题。有什么问题?让我们看看下面的问题;创建和销毁线程资源消耗;我们使用线程的目的是为了效率的考虑。为了创建这些线程,会消耗额外的时间和资源,线程的销毁也需要系统资源。cpu资源有限,上面代码创建的线程过多,导致部分任务没有立即完成,响应时间过长。线程无法管理,无节制地创建线程似乎是对有限资源“得不偿失”的效果。既然我们上面手动创建线程有问题,请问有解决办法吗?答:可以,使用线程池。线程池简介线程池(ThreadPool):一种对一个或多个线程进行统一调度和复用的技术,避免了线程过多带来的开销。线程池的优点是什么?减少资源消耗。通过重用已创建的线程来降低线程创建和销毁的成本。提高响应能力。当任务到达时,可以立即执行任务,而无需等待线程创建。提高线程可管理性。线程池使用JDK中rt.jar包下的JUC(java.util.concurrent)创建线程池的方式有两种:ThreadPoolExecutor和Executors,Executors可以创建6种不同的线程池类型。ThreadPoolExecutor使用线程池代码如下:importjava.util.concurrent.LinkedBlockingQueue;importjava.util.concurrent.ThreadPoolExecutor;importjava.util.concurrent.TimeUnit;publicclassThreadPoolDemo{privatestaticThreadPoolExecutorthreadPoolExecutor=newThreadPoolExecutor(lock2,10,newCONOldLinkB,(100));publicstaticvoidmain(String[]args){threadPoolExecutor.execute(newRunnable(){@Overridepublicvoidrun(){System.out.println("Hello,Mr.Tian");}});}}以上程序的执行结果为如下:田老师您好核心参数说明ThreadPoolExecutor有以下四种构造方法:可以看到最后一个构造方法有7个构造参数。其实前三个构造方法只是包裹了最后一个方法,而前三个构造方法最终调用的是最后一个构造方法,所以这里先说说最后一个构造方法。该参数解释了corePoolSize线程池中的核心线程数。默认情况下,核心线程一直存活在线程池中。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,如果线程池空闲时间超过keepAliveTime指定的时间,核心线程将被终止。maximumPoolSize是最大线程数,线程不够时可以创建的最大线程数。keepAliveTime是线程池的空闲超时时间。默认情况下,它对非核心线程生效。如果空闲时间超过这个时间,非核心线程就会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut设置为true,核心线程超过空闲时间就会被回收。unit与keepAliveTime一起使用,标识keepAliveTime的时间单位。workQueue线程池中的任务队列,使用execute()或submit()方法提交的任务都会存放在这个队列中。threadFactory为线程池提供了一个线程工厂来创建新的线程。rejectedExecutionHandler线程池任务队列超过最大值后的拒绝策略。RejectedExecutionHandler是一个接口,它只有一个rejectedExecution方法。可以在该方法中添加任务超过最大值的事件处理。ThreadPoolExecutor还提供了4种默认的拒绝策略:DiscardPolicy():丢弃任务而不进行处理。DiscardOldestPolicy():丢弃队列中最新的任务,执行当前任务。AbortPolicy():直接抛出RejectedExecutionException(默认)。CallerRunsPolicy():既不放弃任务也不抛出异常,直接使用主线程执行这个任务。具有所有参数的用例:publicclassThreadPoolExecutorTest{publicstaticvoidmain(String[]args)throwsInterruptedException,ExecutionException{ThreadPoolExecutorthreadPool=newThreadPoolExecutor(1,1,10L,TimeUnit.SECONDS,newLinkedBlockingQueue(2),newMyThreadsPoolThreadFactory(Executor),));threadPool.allowCoreThreadTimeOut(true);for(inti=0;i<10;i++){threadPool.execute(newRunnable(){@Overridepublicvoidrun(){System.out.println(Thread.currentThread().getName());尝试{Thread.sleep(2000);}catch(InterruptedExceptione){e.printStackTrace();}}});}}}classMyThreadFactoryimplementsThreadFactory{privateAtomicIntegercount=newAtomicInteger(0);@OverridepublicThreadnewThread(Runnabler){Threadt=newThread(r);StringthreadName="MyThread"+count.addAndGet(1);t.setName(threadName);return;}}运行输出:mainMyThread1mainMyThread1MyThread1....这只是为了演示所有的参数自定义,没有其他用途。execute()和submit()的使用execute()和submit()都是用来执行线程池的,不同的是submit()方法可以接收线程池执行的返回值。下面看看两种方法的具体使用和区别://创建线程池ThreadPoolExecutorthreadPoolExecutor=newThreadPoolExecutor(2,10,10L,TimeUnit.SECONDS,newLinkedBlockingQueue(100));//执行使用threadPoolExecutor.execute(newRunnable()){@Overridepublicvoidrun(){System.out.println("HelloLaotian");}});//使用Futurefuture=threadPoolExecutor.submit(newCallable(){@OverridepublicStringcall()throwsException{System提交.out.println("田先生您好");return"返回值";}});System.out.println(future.get());上面程序的执行结果如下:田老师你好,returnValueExecutorsExecutorsExecutors创建了很多线程池,基本都是简单封装在ThreadPoolExecutor构造方法上,特殊场景根据需要自己创建.Executors可以理解为一个工厂类。执行者可以创建6种不同的线程池类型。下面简单介绍一下这六个方法:newFixedThreadPool创建一个固定数量的线程池,多余的任务会等待队列中的空闲线程,可以用来控制程序的最大并发数。newCacheThreadPool是一个在短时间内处理大量工作的线程池。它会根据任务的数量生成相应的线程,并尝试缓存线程以供重用。如果它们在60秒内未被使用,缓存将被删除。如果没有可用的现有线程,则创建一个新线程并将其添加到池中。如果有一个线程已经被使用但还没有被销毁,就重用该线程。终止并从缓存中删除60秒内未使用的线程。因此,长时间处于空闲状态的线程池不会使用任何资源。newScheduledThreadPool创建一个固定数量的线程池,用于支持定时或周期性任务的执行。newWorkStealingPoolJava8添加了一个创建线程池的新方法。如果创建时不设置参数,则使用当前机器CPU处理器号作为线程数。这个线程池会并行处理任务,不能保证执行顺序。newSingleThreadExecutor创建一个单线程线程池。这个线程池只有一个线程在工作,相当于用一个线程串行执行所有任务。如果唯一的线程异常结束,一个新的线程将取代它。这个线程池保证所有任务的执行顺序按照任务提交的顺序执行。newSingleThreadScheduledExecutor这个线程池是单线程newScheduledThreadPool。如何关闭线程池?要关闭线程池,可以使用shutdown()或shutdownNow()方法。它们的区别在于:shutdown():线程池不会立即终止,而是会等到任务队列中的所有任务都执行完才会终止。shutdown方法执行后,线程池将不再接受新的任务。shutdownNow():执行该方法时,线程池的状态立即变为STOP状态,并尝试停止所有正在执行的线程,不再处理仍在池队列中等待的任务。执行此方法将返回未执行的任务。下面的代码用于模拟shutdown(),向线程池中添加任务。代码如下:importjava.util.concurrent.*;importjava.util.concurrent.atomic.AtomicInteger;publicclassThreadPoolExecutorAllArgsTest{publicstaticvoidmain(String[]args)throwsInterruptedException,ExecutionException{//创建线程池ThreadPoolExecutorthreadPoolExecutor=newThreadPoolExecutor(1,1,10L,TimeUnit.SECONDS,newLinkedBlockingQueue(2),newMyThreadFactory(),newThreadPoolExecutor.CallerRunsPolicy());threadPoolExecutor.allowCoreThreadTimeOut(true);//提交任务threadPoolExecutor.execute(()->{for(inti=0;i<3;i++){System.out.println("提交任务"+i);try{Thread.sleep(3000);}catch(InterruptedExceptione){System.out.println(e.getMessage());}}});threadPoolExecutor.shutdown();//引用任务threadPoolExecutor.execute(()->{System.out.println("我要再提任务和任务");});}}执行结果上面的程序如下:提交任务0提交任务1提交任务2可以看出,shutdown()之后,不会再接受新的任务,而是将之前的任务执行完毕。面试题面试题1:ThreadPoolExecutor的常用方法有哪些?ThreadPoolExecutor有以下常用方法:submit()/execute():执行线程池shutdown()/shutdownNow():终止线程池isShutdown():判断线程是否终止getActiveCount():运行线程数getCorePoolSize():获取核心线程数getMaximumPoolSize():获取最大线程数getQueue():获取线程池中的任务队列allowCoreThreadTimeOut(boolean):设置空闲时是否回收核心线程这些方法可以用来终止线程池,线程池监控等面试题2:submit(和execute有什么区别?submit()和execute()都是用来执行线程池的,但是用execute()执行线程池不能有return方法,使用submit()可以使用Future接收线程池执行的返回值。说说创建线程池需要的核心参数的含义ThreadPoolExecutor最多包含以下7个参数:corePoolSize:线程池核心线程数maximumPoolSize:线程池最大线程数keepAliveTime:空闲超时单位:keepAliveTime超时单位(时/分/秒等)workQueue:线程池中的任务队列threadFactory:为线程池提供新线程创建的线程工厂rejectedExecutionHandler:线程池任务队列超过最大值后的拒绝策略面试题3:shutdownNow()和shutdown()这两个方法有什么区别?ShutdownNow()和shutdown()都是用来终止线程池的。它们的区别在于程序在使用shutdown()时不会报错或立即终止线程。等待线程池中缓存的任务执行完毕后退出。执行shutdown()后,不能再向线程池中加入新的任务;shutdownNow()将尝试立即停止任务。如果正在执行的线程池中还有缓存的任务,则会抛出java.lang.InterruptedException:sleepinterruptedException异常。面试题6:你了解线程池的工作原理吗?当线程池中有任务需要执行时,线程池会判断如果线程数不超过核数,则创建新的线程池执行任务。如果线程池中的线程数已经超过核心线程数,则将任务放入任务队列中,排队等待执行;如果任务队列超过最大队列数,而线程池没有达到最大线程数,则会创建一个新的线程来执行任务;如果超过最大线程数,则执行拒绝执行策略。面试题5:如何设置线程池的核心线程数?“CPU密集型任务”:比如加解密、压缩、计算等一系列需要大量CPU资源的任务,在大多数场景下都是纯CPU计算。尽量使用较小的线程池,一般是CPU核心数+1。因为CPU密集型任务使得CPU使用率非常高,如果开启过多的线程,会造成CPU切换过多。“IO密集型任务”:比如MySQL数据库、文件读写、网络通信等任务,这些任务不会特别消耗CPU资源,但IO操作比较耗时,会占用较多的时间。可以使用稍微大一点的线程池,一般为2*CPU核心数。IO密集型任务CPU使用率不高,所以CPU在等待IO的同时可以有其他线程去处理其他任务,充分利用CPU时间。另外:线程平均工作时间的比例越高,需要的线程越少;线程平均等待时间的比例越高,需要的线程越多;以上仅为理论值,建议使用本地或测试环境多次调优,找到一个相对理想的值。面试题7:线程池为什么要用(阻塞)队列?主要有3点:因为如果无限创建线程,可能会因为内存占用过多而导致OOM,cpu切换过多。创建线程池的成本很高。面试题8:为什么线程池使用阻塞队列而不是非阻塞队列?阻塞队列可以保证当任务队列中没有任务时,阻塞获取任务的线程,使线程进入等待状态,释放cpu资源。当队列中有任务时,相应的线程就会被唤醒,从队列中取出消息执行。这样线程就不会一直占用cpu资源。(线程执行完任务后,通过循环再次从任务队列中取出任务执行,代码片段如下while(task!=null||(task=getTask())!=null){}).不使用阻塞队列也是可以的,但是实现起来比较麻烦。既然有用,为什么不用呢?面试题9:你了解线程池的现状吗?通过获取线程池的状态,可以判断线程池是否正在运行,是否可以添加新的任务以及线程池的优雅关闭等。RUNNING:线程池的初始化状态,可以添加任务被执行。SHUTDOWN:线程池处于挂起关闭状态,不接收新任务,只处理接收到的任务。STOP:线程池立即关闭,不接收新任务,放弃缓存队列中的任务,中断正在处理的任务。TIDYING:线程池独立组织状态,调用terminated()方法组织线程池。TERMINATED:线程池终止状态。面试题10:你知道线程池中线程复用的原理吗?线程池将线程和任务解耦。线程是线程,任务是任务。摆脱了通过Thread创建线程时一个线程必须对应一个任务的限制。在线程池中,同一个线程可以不断地从阻塞队列中获取新的任务去执行。核心原理就是线程池对Thread进行了封装。它不会在每次执行任务时调用Thread.start()来创建新线程。而是让每个线程执行一个“循环任务”。在这个“循环任务”中,它不断地检查是否有任务需要执行。作为普通方法执行,这样只用一个固定的线程串联所有任务的run方法。本文总结不使用线程池的缺点,Executors介绍,Executors的6个方法介绍,如何使用线程池,理解线程池原理,核心参数,10线程池面试问题。本文转载自微信公众号《Java后端技术全栈》,可通过以下二维码关注。转载本文请联系Java后端技术全栈公众号。