三种线程池的内部实现&有界/无界队列

本文介绍了Java中三种线程池的内部实现及应用场景,包括固定大小线程池newFixedThreadPool、单线程线程池newSingleThreadExecutor和缓存线程池newCachedThreadPool。线程池通过调整线程数量和使用不同类型的等待队列(如SynchronousQueue、ArrayBlockingQueue、LinkedBlockingQueue和PriorityBlockingQueue)来管理任务执行,以提高系统性能并避免频繁创建销毁线程。同时,线程池的拒绝策略也在处理任务过多的情况中起到关键作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

以前学.NET时,为了不用每次连接数据库都去新建一个连接字符串,老师让我们把数据库连接字符串保存到webconfig文件里。现在看多线程,Java虽然有自动回收,线程在run()方法执行完后就会自动回收,但是线程的创建和销毁都是需要时间,如果大量地创建线程,不光需要系统花费大量的内存,线程的创建与销毁也要浪费很多的时间,最后可能让程序运行的很慢。

既然频繁的创建和销毁线程会降低系统性能,那么有没有什么办法可以解决这个问题?对于不需要用的线程,我们可不可以让它一直呆在那里等待任务的到来?方法肯定有,java.util.concurrent包里有一个Executors类,可以得到某种“线程池”,什么是线程池?在线程池里,有一个或几个空闲的线程,当有任务提交后,从线程池里拿出一个线程T1,完成任务后,线程T1放回线程池中,变成空闲线程,等待下一个任务的到来。

线程池的作用就是避免了频繁大量地创建和销毁线程,导致CPU浪费大量的时间,性能不佳。线程池中的线程可以复用,也就是每时每刻都有活跃的空闲线程等待任务的提交然后去执行。我们可以根据实际情况规定线程池中线程的数量,如果线程池中线程太多,但实际又用不上那么多,就可以在实例化时规定线程池中的总线程数量,防止内存资源浪费。如果线程池中空闲线程不够,线程池也提供了一个任务等待队列,例如在newSingleThreadExecutor()方法中实例化的线程池就有一个按照“先进先出”的任务等待队列。这篇日志先来说说我用到的三种线程池:

  • public static ExecutorService newFixedThreadPool(int nThread):该方法,得到的是一个固定大小的线程池,线程池里的线程数量始终维持在一个固定值,当有一个心得任务进来后,如果线程池中有空闲线程,就立刻调出线程执行任务;否则就会把任务交到等待队列中,等待有空闲线程后再调度执行。
  • public static ExecutorService newSingleThreadExecutor():该方法得到的是一个只有一个线程的线程池,如果有任务提交,当线程池内有空闲线程,任务会被执行,否则就会存到任务队列中等待空闲线程。
  • public static ExecutorService newCachedThreadPool():该方法得到的线程池里线程数量是不固定的,会根据实际情况调整线程池里的线程数量,如果线程池里有空闲线程,当有任务被提交后,会优先使用线程池里的空闲线程,如果线程池里所有的线程都有任务在身,此时又有新任务提交,那么线程池就会新建一个新的空闲线程来执行任务。

 

首先来看看第一种返回固定数量的线程池的使用代码:

看main函数里,第36行创建了一个线程池ES,用newFixedThreadPool()方法,并传入参数4,也就是线程池里固定4个线程。接着for循环往线程池里提交一共10个任务线程,线程要做的事情就是打印出自己的执行时间,睡眠一秒后再打印出自己的结束运行时间。从运行结果来看,线程池里ID从1-4号的线程都依次被拿出执行任务,往后的线程ID都是线程池内的1-4编号,证明线程池内固定只有4个线程。

 

再来看看第二种,只有一个线程的线程池:

这个类似,main函数里往这个线程池中提交5个任务,可以看到运行结果,线程ID始终是1号,证明了线程池里一直只有一个线程。

至于第三种线程池newCacheThreadPool(),会根据实际情况调整线程池内的线程,所以里面线程数量不确定,这里就不演示了。

 

三种线程池的调用很简单,翻看它们的源码,发现它们的内部都是返回了一个ThreadPoolExecutor来实现,只是调用时传递的参数不同,就能实现不同功能的线程池。

	public static ExecutorService newFixedThreadPool(int nThreads) {
		return new ThreadPoolExecutor(nThreads, nThreads, OL, TimeUnit.MILLISECONDS, 
											new LinkedBlockingQueue<Runnable>());
	}
	
	public static ExecutorService newCachedThreadPool() {
		return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, 
										new SynchronousQueue<Runnable>());
	}
	
	public static ExecutorService newSingleThreadPool() {
		return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 
										OL, TimeUnit.MILLISECONDS, 
										new LinkedBlockingQueue<Runnable>()));
	}

于是翻看了网上的源码解读,首先我们来看看这个ThreadPoolExecutor的构造函数,因为上面的三种线程池都调用到了它:

	public ThreadExecutor(int corePoolSize, //线程池中的线程数
		    int maximumPoolSize, //线程池中的最大线程数
		    long keepAliveTime, //线程数超过指定数值后,多余的空闲线程的存活时间
		    TimeUnit unit, //线程池维护线程所允许的空闲时间的单位
		    BlockingQueue<Runnable> workQueue, //被提交但未被执行的任务等待队列
		    ThreadFactory threadFactory, //线程工厂,用于创建线程
            RejectedExecutionHandler handler)  //拒绝策略,当提交的任务太多,不够线程处理后,如何拒绝任务

三种线程池的不同功能是如何实现,关键是ThreadPoolExecutor里面的任务等待队列workQueue和拒绝策略handler。workQueue队列是已提交但未被执行的任务的等待队列,接口是BlockingQueue,存放的类型的Runnable对象,BlockingQueue任务等待队列有四种:

  1. 直接提交队列:使用的是SynchronousQueue类实现的队列。提交到这个队列中的任务不会被真实的保存,而是立刻将新任务提交个线程执行,如果没有空闲的线程,就会创建新的线程,如果线程数量达到线程池的最大线程数maximumPoolSize,那么就会执行拒绝策略,任务执行被拒绝。
  2. 有界的任务队列:使用的是ArrayBlockingQueue类实现的队列,按照“先进先出”算法处理任务,它的构造函数是:

public ArrayBlockingQueue(int capacity)

使用它必须设置一个最大容量参数,使用有界的任务队列的线程池,当有任务提交后,如果当前线程池中的线程数小于corePoolSize,就会创建新的线程执行任务,如果任务提交后,当前线程池内线程数大于corePoolSize,就会把任务先放到有界任务队列中,若队列放满后,再创建新线程执行任务,但总的线程数不会超过最大值maximumPoolSize。

  1. 无界的任务队列:使用的是LinkedBlockingQueue类实现,不需要事先制定大小,也是按照“先进先出”算法处理任务。无界队列很好理解,就是和有界队列相反,使用无界队列的线程池,当有新任务提交时,如果线程池里有空闲线程,就分配线程立刻执行任务,否则就把任务放到无界任务队列中等待,如果线程池中一直没有空闲线程,但是新的任务又一直不停的提交上来,那么这些任务全部会被挂到等待队列中,一直到内存全部消耗完。
  2. 优先任务队列:使用的是PriorityBlockingQueue类实现,它根据任务的优先级顺序执行任务,不同于前面的有界队列和无界队列。

大概了解了这个关键的任务队列参数后,来看看它是怎么决定三种线程池的不同功能的?首先是newFixedThreadPool()

	public static ExecutorService newFixedThreadPool(int nThreads) {
		return new ThreadPoolExecutor(nThreads, nThreads, OL, TimeUnit.MILLISECONDS, 
											new LinkedBlockingQueue<Runnable>());
	}

对照ThreadPoolExecutor的参数,它传入的corePoolSize和maximumPoolSize大小一样,都是nThreads,因为是固定大小的线程池,所以线程数corePoolSize和最大线程数maximumPoolSize值一样。第三个参数0L表示当线程池数量超过corePoolSize后,多余的空闲线程的存活时间为0,即立刻被停止,这样来做到控制线程池内数量的固定。第四个参数使用的是LinkedBlockingQueue类的队列,也就是无界队列,那么意思是这样的线程池,在线程数量达到corePoolSize后,往后提交的任务都会被放到无界队列中等待,不会拒绝,直到内存用完为止。

再看newCachedThreadExecutor():

	public static ExecutorService newCachedThreadPool() {
		return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, 
										new SynchronousQueue<Runnable>());
	}

传入的corePoolSize是0,maximumPoolSize是无穷大,第三个参数60L,意思是超过corePoolSize的空闲线程会在60秒内被回收,第四个参数使用的是SynchronousQueue直接提交队列。这样的线程,意思是没有任务提交时,线程池里线程数为0,没有线程。让有任务提交后,线程池会调用空闲线程去执行任务,如果线程池中没有空闲线程,,任务就会放到直接提交队列SynchronousQueue中,而直接提交队列会在线程池线程数没有达到maximumPoolSize的前提下去新建新的空闲线程执行任务,上面参数maximumPoolSize又是无穷大,所以任务会得到执行,并且任务执行完毕后,变为空闲线程,由于corePoolSize的值为0,即线程池里线程数为0,所以空闲线程会被当作多余的空闲线程,在60秒过后被回收。

最后的newSingleThreadPool():

	public static ExecutorService newSingleThreadPool() {
		return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 
										OL, TimeUnit.MILLISECONDS, 
										new LinkedBlockingQueue<Runnable>()));

它就是把corePoolSize和maximumPoolSize都设置为1,来保证线程池中始终只有一个线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值