Java线程池原理和使用总结

为什么需要线程池

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
  2. 提升响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行;
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

实现一个简单的线程池

下面通过一个简单的demo直观的感受下线程池的实现原理。jdk中提供的线程池比这复杂多了。

线程池:

package lihao.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class MyThreadPool {
	
	private BlockingQueue<Runnable> taskQueue;
	
	private List<PoolThread> threads = new ArrayList<>();
	
	private volatile boolean isStopped = false;
	
	public MyThreadPool(int numOfThreads, int maxNumOfTasks) {
		taskQueue = new ArrayBlockingQueue<>(maxNumOfTasks);
		
		for (int i = 0; i < numOfThreads; i++) { 
			threads.add(new PoolThread(taskQueue)); 
		}
		
		for (PoolThread thread : threads) {
			thread.start();
		}
	}
	
	public synchronized void execute(Runnable task) {
		if (this.isStopped)
			throw new IllegalStateException("ThreadPool is stopped");
		this.taskQueue.offer(task);
	}
	
	public synchronized void stop() {
		this.isStopped = true;
		for (PoolThread thread : threads) {
			thread.toStop();
		}
	}

}

线程池中的工作线程:

package lihao.thread;

import java.util.concurrent.BlockingQueue;

public class PoolThread extends Thread {
	
	private BlockingQueue<Runnable> taskQueue = null; 

	private volatile boolean isStopped = false; 

	public PoolThread(BlockingQueue<Runnable> taskQueue) {
		this.taskQueue = taskQueue;
	}
	
	public void run() {
		while (!isStopped) {
			Runnable task;
			try {
				task = taskQueue.take();
				task.run();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public synchronized void toStop() {
		isStopped = true; 
		this.interrupt(); // 打断taskQueue.take()调用
	} 

	public synchronized boolean isStopped() { 
		return isStopped; 
	} 

}

线程池实现原理

在这里插入图片描述
当向线程池提交一个任务后,线程池是如何处理这个任务的呢?通过上图可以看出,当提交一个新任务到线程池时,线程池的处理流程如下:

  1. 线程池判断核心线程池是否已满。如果没有满,则创建一个新的工作线程来执行任务(执行这一步需要获取全局锁)。如果核心线程池已满,则进入下一个流程;
  2. 线程池判断工作队列是否已满。如果工作队列没满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程;
  3. 线程池判断线程池里的线程个数是否达到最大数目。如果没有达到最大数目,则创建一个新的线程来执行任务(执行这一步需要获取全局锁)。如果达到最大线程数,则交给饱和策略来处理。

线程池采取上述步骤的总体设计思路是为了在提交任务执行时尽可能的避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在线程池完成预热后(核心线程池已满),几乎所有的任务都会提交到工作队列,而这一步不需要获取全局锁。

线程池的使用

常用实现类

ThreadPoolExecutor是线程池的核心实现类,下面是它的构造器:

ThreadPoolExecutor(int corePoolSize,
                   int maximumPoolSize,
                   long keepAliveTime,
                   TimeUnit unit,
                   BlockingQueue<Runnable> workQueue,
                   ThreadFactory threadFactory,
                   RejectedExecutionHandler handler)

参数解释如下:

corePoolSize:线程池的基本大小。当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到工作线程数等于线程池基本大小时就不再创建线程。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有工作线程。

maximumPoolSize:线程池允许创建的最大线程数。如果任务队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程来执行任务。值得注意的是,如果使用了无界的任务队列,这个参数就没什么效果。

keepAliveTime:当线程数大于corePoolSize后,所多出来的那些线程的最长空闲时间,超过这个时间还没有获取到任务来执行,多出来的线程就会停止。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。

unit:keepAliveTime的单位。

workQueue:用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:

  1. ArrayBlockingQueue: 是一个基于数组结构的阻塞队列。此队列按FIFO(先进先出)原则对元素进行排序;
  2. LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列;
  3. SynchronousQueue::一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列;
  4. PriorityBlockingQueue:一个具有优先级的阻塞队列。

threadFactory:创建工作线程的工厂。可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

handler:线程池饱和策略。当任务队列和线程池都满了,线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。Java线程池框架提供了以下四种策略:

  1. AbortPolicy:直接抛出异常;
  2. CallerRunsPolicy:只用调用者所在线程来运行任务;
  3. DiscardOldestPolicy:丢弃队列里最近的(提交时间最早的)一个任务,并执行当前任务;
  4. DiscardPolicy:不处理,丢弃掉

当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。

线程池种类

我们在使用线程池时,一般很少直接通过ThreadPoolExecutor的构造器创建,而是使用工厂类Executors来创建。Executors可以创建3种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool。

  1. FixedThreadPool:固定线程数的线程池。适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。FixedThreadPool的corePoolSize和maximumPoolSize都是参数nThreads。keepAliveTime是0。workQueue是LinkedBlockingQueue,它的容量是默认值,也就是int的上限,所以它是一个无界阻塞队列。下面是Executors提供的创建方法:
public static ExecutorService newFixedThreadPool(int nThreads) 
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
  1. SingleThreadExecutor: 单个线程的线程池。适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多 个线程是活动的应用场景。SingleThreadExecutor的corePoolSize和maximumPoolSize都是参数1,其他参数跟上述FixedThreadPool一样。下面是Executors提供的创建方法:
public static ExecutorService newSingleThreadExecutor() 
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
  1. CachedThreadPool:根据需要创建新线程的线程池。CachedThreadPool是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。CachedThreadPool的corePoolSize是0,maximumPoolSize是int的上限,keepAliveTime是60秒,workQueue用的是SynchronousQueue.下面是Executors提供的创建方法:
public static ExecutorService newCachedThreadPool() 
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)

合理的配置线程池

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

  1. 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
  2. 任务的优先级:高、中和低。
  3. 任务的执行时间:长、中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的 线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务 和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高 的任务先执行。注意,如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能被执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。
建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要将队列容量设置大一点儿,比如几千。如果使用无界队列, 有可能会撑满内存,导致整个系统不可用。

猜你喜欢

转载自blog.csdn.net/qq_35939417/article/details/104684987