所有栏目 | 云社区 美国云服务器[国内云主机商]
你的位置:首页 > 云社区 » 正文

在Java并发编程中,如何扩展和优化线程池?

发布时间:2020-04-15 16:44:13

资讯分类:线程池  java  编程  并发  线程  数量  多线程
在Java并发编程中,如何扩展和优化线程池?

在java中多线程并不陌生,在一定的范围内,多线程数量的增加会明显提升整个系统的吞吐性能,但是线程本身会极大的耗费内存空间,线程的频繁创建和回收也极其占用CPU资源,多线程甚至会拖垮整个服务!

所以,线程的利用必须掌握在一个度,太少的线程数可能会浪费CPU资源,而太高也极有可能反而降低整个应用性能;

线程池:基于使用多线程存在的问题,JDK提出了线程池技术,类似于数据库连接池,都是保持池中部分线程活跃状态,在需要使用线程的时候,直接从线程池中获取,使用。当线程使用结束,就进行回收(直接放回池中等待,而不是GC),这样就能避免了线程的频繁创建和回收。

JAVA中的线程池:JDK提供了线程池框架Executor,帮助程序更好的管理线程。总的结构如下截图:

比较常见的线程池对象获取方式为:

①newSingleThreadExecutor():返回单线程的线程池,一个接一个的处理任务,线程异常的时候,会创建新的线程替代; ②newFixedThreadPool:在达到最大线程之前,有一个任务就创建一个线程,直到达到最大线程数量; ③newCachedThreadPool:动态的设置最合适的线程数量,最大为JVM能够支持的大小; ④newScheduledThreadPool:指定线程数量,并周期性的执行任务; ⑤newSingleThreadScheduledExecutor:指定线程数量1个,并周期性的执行任务;

从源码来看,上面几种线程池底层都是封装的ThreadPoolExecutor对象,查看源码可知比较重要的属性(对象)截图如下:



定义了线程池中的线程数量,最大线程池数量,线程工厂(用于线程的创建),workQuere任务队列,handler拒绝策略等属性,用于线程池的对象初始化和任务调度!

下图是ThreadPoolExecutor对象中的execute方法截图:

解释如下:

1,当前线程总数小于核心线程数,则通过addWorker进行执行;

2,否则通过wordQueue.offer提交到等待队列,

3,进入等待队列失败,则通过addWorker提交到线程池,失败则执行拒绝策略;

线程池有多种拒绝策略:直接抛出异常,或者丢弃无法处理的任务等等,此处不做详细讨论。。

线程池的扩展:JDK允许开发人员自主扩展线程池,通过提供的beforeExecute,afterExecute,terminated三个接口可以像处理AOP一样方便的管理线程池,可自行实现状态跟踪,调试信息等用以监控线程池!

线程池的优化:线程池的优化主要针对线程数量进行,一般来说只要使用的不是最大最小线程数量都可以,但是具体的还要根据场景,参考CPU核心数,等待时间等因素来判断最合适的线程数,比如是批量运算这种密集的CPU执行,则线程数设置为CPU核心数即可,如果有大量阻塞,则可以使用CPU核心数的偶数倍数,在有一本书中得出了一个公式如下截图:

jdk中的线程池技术比较完善,加上其他的多线程技术,促使JAVA成为高并发领域的佼佼者,最近一直在分享JAVA技术,得到很多朋友的鼓励,在此表示感谢,我也会一直持续的进行分享,敬请关注。。

在Java并发编程中,如何扩展和优化线程池?

笔者从事Java开发多年,在Java并发编程中积累了一定的经验,就线程池扩展优化问题,来分享一下自己的一些看法。

线程池利用好了,可以高系统性能,如果利用不好,也会带来一系列问题。今天我们从6个方面来谈线程池的优化。

1. 核心线程WarmUp优化

默认情况下,核心工作线程值在初始的时候被创建,当新任务到来的时候被启动,但是我们可以通过重写prestartCoreThread或prestartCoreThreads方法来改变这种行为。通常情况下我们可以在应用启动时来WarmUp核心线程,从而达到任务过来能够立即执行的结果,使得初始任务处理的时间得到一定优化。

2. 定制工作线程的创建优化

新的线程是通过ThreadFactory来创建的,如果没有指定,默认会使用Executors#defaultThreadFactory,这时创建的线程将都属于同一个线程组,拥有同样的优先级和daemon状态。通过扩展配置ThreadFactory,我们可以配置线程的名称、线程组合daemon状态。如果调用ThreadFactory#createThread失败,将返回null,executor将不会执行任何任务。

3. 核心线程回收

如果当前池子中的工作线程数大于corePoolSize,超过corePoolSize的线程处于空闲的时间大于keepAliveTime,则这些线程将会被终止,这是一种减少不必要资源消耗的策略。这个参数可以在运行时被改变,我们同样可以将这种策略应用给核心线程,可以通过调用allowCoreThreadTimeout来实现。

4. 正确的选择队列

下面主要是不同队列策略表现:

4.1 直接递交:一种比较好的默认选择是使用SynchronousQueue,这种策略会将提交的任务直接传送给工作线程,而不持有。如果当前没有工作线程来处理,即任务放入队列失败,则根据线程池的实现,引发新的工作线程创建,新提交的任务就会被处理。这种策略在当提交的一批任务之间有依赖关系时能避免锁竞争消耗。值得一提的是,这种策略最好配合unbounded线程数来使用,从而避免任务被拒绝。同时我们必须要考虑到一种场景,当任务到来的速度大于任务处理的速度时,将会引起无限制的线程数不断增加的问题。

4.2 无界队列:使用无界队列如LinkedBlockingQueue没有指定最大容量时,将会引起当核心线程都在忙的时候,新的任务被放在队列上,因此,永远不会有大于corePoolSize的线程被创建,maximumPoolSize参数将失效。这种策略比较适合所有的任务都不相互依赖、独立执行的情况。举个例子,网页服务器中,每个线程独立处理请求。但是当任务处理速度小于任务进入速度时会引起队列的无限膨胀。

4.3 有界队列:有界队列如ArrayBlockingQueue帮助限制资源的消耗,但是不好控制。队列长度和maximumPoolSize这两个值会相互影响,使用大的队列和小maximumPoolSize会减少CPU的使用、操作系统资源、上下文切换的消耗,但是会降低吞吐量。如果任务被频繁的阻塞,系统则可以调度更多的线程。使用小的队列通常需要大maximumPoolSize,从而使得CPU更忙一些,但是又会增加降低吞吐量的线程调度的消耗。

总结一下是IO密集型可以考虑多些线程来平衡CPU的使用,CPU密集型可以考虑少些线程减少线程调度的消耗。

5. 合理的配置线程池

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  • 任务性质:CPU密集型任务,IO密集型任务和混合型任务;

  • 任务优先级:高,中和低;

  • 任务执行时间:长,中和短;

  • 任务依赖性:是否依赖其他系统资源,如数据库连接等。

任务性质不同的任务可以用不同规模的线程池分开处理。

  • CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池。

  • IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。

  • 混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

建议使用有界队列,有界队列能增加系统的稳定性和预警能力。

6. 线程池的监控

通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用:

  • taskCount:线程池需要执行的任务数量。

  • completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。

  • largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。

  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不+ getActiveCount:获取活动的线程数。

  • 通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。如protected void beforeExecute(Thread t, Runnable r) { }

欢迎关注笔者,持续分享有价值的优质架构文章。

在Java并发编程中,如何扩展和优化线程池?

就是放线程的一个容器,每次创建线程很浪费系统资源。用的时候从线程池取,用完了就放回去就行了。

在Java并发编程中,如何扩展和优化线程池?

线程池创建和销毁是有代价的,所以可以通过提前创建线程池来缓解这个问题。但是创建多少个是个问题?

一般根据业务复杂度,比如提前创建100个,然后设置一个低水位和高水位,比如20% 和80%,当达到低水位且持续一段时间,就可以释放一部分。当高水位一段时间后,可以动态增加一部分。同时增加手动设置的api可以根据预测提前调整。

留言与评论(共有 0 条评论)
   
验证码:
Top