当前位置: 首页 > 后端技术 > Java

一篇文章看懂Java线程池

时间:2023-04-01 20:33:20 Java

一、创建线程的方式1继承Thread类,重写run方法。实现简单,但不符合里氏代换原则,不能继承其他类。步骤:(1)继承Thread类,重写run方法。run方法的方法体表示线程要完成的任务。因此,run()方法被称为执行体。(2)创建线程对象并调用start方法启动2、实现Runnable接口,重写run方法。它避免了单一继承的限制,使编程更加灵活,实现了解耦。步骤:(1)实现Runnable接口,重写run方法(2)创建线程对象,调用start方法启动3实现Callable接口,重写call方法。可以获得线程执行结果的返回值,可以抛出异常。步骤:(1)定义一个实现了Callable接口的类,实现了call()方法。call()方法会作为线程执行体,有返回值。(2)创建线程对象,使用FutureTask类包裹Callable对象,调用start方法启动FutureTaskft=newFutureTask<>(mc);(3)调用FutureTask对象的get()方法获取子线程执行后的返回值4使用Executors工具类创建线程池2.我们为什么要有线程池想想我们没用的时候之前的线程池,我们每次创建一个线程:newThread(()->{...}),然后调用start()方法执行线程。这会带来一系列的问题,比如:线程的创建和销毁都是非常耗时和浪费性能的操作。再者,简单的新建两个或三个Thread还好,但如果需要上百个线程呢?而当他们用完再销毁时,也必须一一销毁。那么创建和销毁数百个线程性能就很差了!线程池就是为解决上述问题而诞生的。它的核心思想是:线程复用。即线程用完后不销毁,放在池中等待新任务的到来,重复使用N个线程执行所有新老任务。这样带来的开销只会是那N个线程的创建,而不是每次请求都让一个线程从生到死。因此,使用线程池的好处和优点:减少资源消耗。通过重用已创建的线程来降低线程创建和销毁的成本。提高响应能力。当任务到达时,可以立即执行任务,而无需等待线程创建。提高线程可管理性。使用线程池可以统一分配、调优和监控线程。3、创建线程池的方法3.1通过Executors的静态工厂方法可以使用七个参数创建线程池:不推荐使用Executors创建,建议使用ThreadPoolExceutor。这种处理方式让写作的同学更加清楚线程池的Run规则,避免资源耗尽的风险。源码(有七个参数):publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueueworkQueue,ThreadFactorythreadFactory,RejectedExecutionHandlerhandler){}①corePoolSize:线程池后的核心线程数打开后,线程池中的线程数为0。当有任务到来时,会创建一个线程来执行该任务。核心线程数定义了可以同时运行的最小线程数。当线程池中的线程数达到corePoolSize时,到达的任务将被放入工作队列。默认不会被回收,但是如果allowCoreTimeOut设置为true,那么当核心线程空闲的时候,也会被回收。②maximumPoolSize:最大线程数。当队列中存放的任务达到队列容量时,当前可同时运行的线程数成为最大线程数。如果设置为与核心线程数相同,则代表固定大小的线程池。③workQueue:工作队列当线程请求数大于等于corePoolSize时,线程会进入工作队列。阻塞队列用于存放等待执行的任务。一个新的任务提交后,会先进入这个工作队列,然后在任务调度的时候从队列中移除任务。这里的阻塞队列有以下几个选项:ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序;LinkedBlockingQueue:基于链表的无界阻塞队列(实际上最大容量为Interger.MAX),FIFO排序;SynchronousQueue:非阻塞队列缓存任务的阻塞队列,也就是说当有新的任务进来时,不会被缓存,而是直接被调度去执行该任务;PriorityBlockingQueue:无界阻塞队列,有优先级,优先级通过参数Comparator实现。④unit:unit,keepAliveTime的时间单位。例如:TimeUnit.MILLISECONDS、TimeUnit.SECONDS⑤keepAliveTime:线程空闲时间线程空闲时间达到这个值,就会被销毁,直到只剩下corePoolSize个线程,以免浪费内存资源。⑥threadFactory:线程工厂当线程池需要新线程时,会使用threadFactory生成新线程。默认使用DefaultThreadFactory,主要负责创建线程。新线程()方法。创建的线程都在同一个线程组中,具有相同的优先级。也就是说,线程用于产生一组相同的任务。可以命名线程以帮助分析错误。⑦handler:拒绝策略。AbortPolicy丢弃任务并抛出异常;功能:当触发拒绝策略时,直接抛出拒绝执行的异常。abortpolicy的意思是打断当前的执行过程。使用场景:这个没有特殊场景,但是有一点一定要正确处理抛出的异常。ThreadPoolExecutor中的默认策略是AbortPolicy。ThreadPoolExecutor系列的ExecutorService接口没有显示拒绝策略,所以默认就是这个。但是需要注意的是,ExecutorService中的线程池实例队列是无界的,也就是说内存爆了不会触发拒绝策略。当你自己自定义线程池实例时,在使用该策略时必须处理策略触发时抛出的异常,因为它会打断当前的执行过程。CallerRunsPolicy调用执行自己的线程来运行任务。但是这种策略会降低提交新任务的速度,影响程序的整体性能。此外,此策略喜欢增加队列容量。如果你的应用可以承受这个延迟,并且你不能丢弃任何任务请求,你可以选择这个策略;作用:当触发拒绝策略时,只要线程池没有关闭,就会由当前提交任务的线程处理。使用场景:一般用于不允许失败,性能要求不高,并发量小的场景,因为一般情况下线程池是不会关闭的,即提交的任务肯定会运行,但是因为是调用者线程自己执行的。当多次提交任务时,后续任务的执行就会被阻塞,性能和效率自然会变慢。DiscardOldestPolicy表示丢弃队列中等待时间最长的任务,将当前任务加入队列;功能:直接悄悄丢弃这个任务,不触发任何动作使用场景:如果你提交的任务无关紧要,你可以使用它。因为它只是一个空的实现,它会默默地吞噬你的任务。所以这个策略基本没有使用DiscardPolicy,也就是直接丢弃当前任务,不会抛出异常。功能:如果线程池没有关闭,弹出队头元素,然后尝试执行使用场景:该策略仍然会丢弃任务,丢弃时会静默,但是特点是它丢弃旧的未执行的任务,并且是具有更高优先级的任务被执行。基于这个特性,我能想到的场景就是发布消息和修改消息。消息发布时,尚未执行。这时,更新的消息又来了。此时未执行消息的版本高于当前提交的消息,如果版本较低,则可以丢弃。因为队列中可能会有消息版本较低的消息会被排队执行,所以在实际处理消息的时候需要做好消息版本的比较。3.2例子3.3可以这样创建几个内置封装的线程池:ExecutorServiceMyExecutorService=Executors.newCachedThreadPool();newFixedThreadPool,固定大小的线程池,核心线程数也是最大线程数,有没有空闲线程,keepAliveTime=0。这个线程池使用的工作队列是一个无界阻塞队列LinkedBlockingQueue,适用于负载比较大的服务器。newSingleThreadExecutor使用单线程,相当于在单线程中串行执行所有任务,适用于需要顺序执行任务的场景。与单线程性能相比:虽然同一个线程在工作,但是使用单线程池效率要高很多。newCachedThreadPool,该方法返回一个线程池,可以根据实际情况调整线程数。线程池中的线程数是不确定的,但是如果有空闲的线程可以重用,那么优先使用可重用的线程。如果所有线程都在工作并且提交了新任务,则将创建一个新线程来处理该任务。当前任务执行完毕后,所有线程都会返回到线程池中重新使用。newScheduledThreadPool:支持periodic和periodictaskexecution,适用于需要多个后台线程执行periodictasks,限制线程数的场景。与newCachedThreadPool的区别在于工作线程不会被回收。原理:ScheduledThreadPoolExecutor先把任务放入一个DelayQueue延迟队列,然后启动一个线程,然后去队列中取循环时间最接近当前时间的任务。ScheduledThreadPoolExecutor维护了一个DelayQueue来存放等待任务。DelayQueue中有一个PriorityQueue,会根据时间的大小进行排序,时间越小越靠前。DelayQueue也是一个无界队列,但是初始大小为16,超过16就会扩容一次。提交任务的三种方式:schedule,在特定时间延迟后执行一次任务scheduledAtFixedRate,定时执行任务(与任务执行时间无关,周期固定)scheduledWithFixedDelay,执行任务有固定的延迟(与任务执行时间有关,延迟4.线程池处理任务的过程①如果核心线程池未满,则创建新线程执行任务。此时workCount=corePoolSize。③如果工作队列已满且线程数小于最大线程数,创建新线程处理任务。此时workCountmaximumPoolSize。线程池在创建线程时,会将线程封装成一个工作线程Worker,Worker完成任务后也会循环获取工作队列中的任务执行。示例:线程池参数配置:核心线程5个,最大线程10个,队列长度100。然后,线程池启动时不会创建任何线程。假设有6个请求进来,会创建5个核心线程处理5个请求,其他没有处理完的进入队列。这时候进来了99个请求,线程池发现核心线程已经满了,而队列还是空的还有99个位置,所以就会有99个请求进入队列,加上刚才那个,刚好是100个。这个时候,5个请求又进来了,线程池又会开辟5个非核心线程来处理这5个请求。目前的情况是RUNNING状态下线程池中的线程数为10,队列中塞满了100个线程。如果此时有另一个请求进来,则直接执行拒绝策略。5.线程池执行和关闭5.1执行任务的线程池中的submit()和execute()方法有什么区别?接收参数:execute()只能执行Runnable类型的任务。submit()可以执行Runnable和Callable类型的任务。返回值:execute()方法用于提交不需要返回值的任务,因此无法判断任务是否被线程池成功执行;submit()方法用于提交需要返回值的任务。线程池将返回一个Future类型的对象。通过这个Future对象,可以判断任务是否执行成功,可以通过Future的get()方法获取返回值。get()方法将阻塞当前线程,直到任务完成,而使用get(longtimeout,TimeUnitunit)方法会阻塞当前线程一段时间,然后立即返回。这个时候,任务可能还没有完成。异常处理:submit()方便异常处理5.2关闭线程池可以调用shutdown()或shutdownNow()方法关闭线程池。原理是遍历线程池中的工作线程,然后一个一个调用线程的interrupt方法,使线程中断,无法响应中断。任务可能永远不会终止。两者的区别shutdownNow首先将线程池的状态设置为STOP,然后尝试停止正在执行或挂起任务的线程,并返回等待执行的任务列表。shutdown只是将线程池的状态设置为SHUTDOWN,然后中断不在执行任务的线程。shutdown一般是调用关闭线程池,shutdownNow如果不是必须要执行的任务可以调用。6、线程池的线程数如何设计?即线程池线程数与(CPU密集型任务和I/O密集型任务)的关系CPU密集型:这种任务一般不会占用大量IO,所以后台服务器可以快速处理,压力就落在了CPU身上。I/O密集型:经常有大量数据的查询和批量插入操作,此时压力主要在I/O上。与CPU密集型的关系:一般情况下,CPU核心数==最大并发执行线程数。在这种情况下(假设CPU核数为n),会有大量的客户端向服务端发送请求,但服务端最多只能同时执行n个线程。所以这种情况下,不需要设置过大的线程池工作队列(工作队列长度=CPU核数||CPU核数+1)。与I/O密集型的关系:由于长时间的I/O操作,线程一直在工作队列中,但不占用CPU,此时有一个CPU空闲。因此,在这种情况下,应该增加线程池工作队列的长度,同时CPU尽量不要空闲,以提高CPU利用率。一般来说,线程池的大小应该怎么设置(线程池初始默认核心线程大小?)(其中N为CPU个数)。如果是CPU密集型应用,线程池大小设置为N+1,如果是IO密集型应用,线程池大小设置为2N+1。