Java中的线程池

什么是线程池
在Java中,如果每一个请求到达就创建一个新线程,那么创建和销毁线程所花费的时间和消耗系统资源都相当大,甚至可能要比在处理实际请求的时间和资源还要多。如果要在JVM中创建非常多的线程,可能会是系统由于过度消耗内存而导致系统资源不足。为了解决这个问题,就有了线程池的概念。线程池的核心逻辑就是提前创建好若干个线程放在一个容器中,如果有任务需要处理,则将任务直接分配给线程池中的线程来执行处理,任务处理完之后这个线程不会被销毁,而是等待后续分配任务。

在开发过程中,合理使用线程池可以带来以下三个好处:

  1. 降低资源消耗:通过重复利用已经创建的线程来降低线程创建和销毁造成的消耗
  2. 提高响应速度:当任务到达后,可以不需要等待线程的创建就能立即执行
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以进行统一分配、调优和监控。

线程池的实现原理
当向线程池提交一个任务后,线程池是如何处理这个任务的呢?线程池的主要处理流程如下图所示:
在这里插入图片描述
由上图可以看到,当一个新任务提交到线程池的时候,线程池的处理流程如下:

  1. 线程池判断核心线程池里的线程是否都在执行任务,如果不是,则创建一个新的线程来执行任务;如果核心线程池中的线程都在执行任务,则进入下一个流程
  2. 线程池判断工作队列是否已满,如果没满,则将新的任务提交到工作队列中;如果工作队列满了,则进入下一个流程
  3. 线程池判断线程池中的线程是否都处于工作状态,如果没有,则创建一个新的线程来执行任务;如果满了,则交给饱和策略来处理这个任务

ThreadPoolExecutor是线程池的核心,提供了线程池的实现。ThreadPoolExecutor.execute方法的示意图如下:
在这里插入图片描述
ThreadPoolExecutor执行execute方法分下面4中情况:

  1. 如果当前运行的线程少于corePoolSize,则创建新的线程来执行任务(这一步需要获取全局锁)
  2. 如果运行的线程数超过corePoolSize,则将当前任务加入到BlockingQueue
  3. 如果当前任务无法加入到BlockingQueue(队列已满),则创建新的线程来执行任务(这一步需要获取全局锁)
  4. 如果创建新线程将使当前运行的线程超过maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法

ThreadPoolExecutor采取上述步骤的总体思路是为了在执行execute()方法时,尽可能的避免获取全局锁;ThreadPoolExecutor在完成预热之后(当前运行的线程大于corePoolSize),几乎所有的execute方法都会执行第二步,而第二步不需要获取全局锁。

源码分析
上面的流程分析让我们很直观的了解了线程池的工作原理,接下来我们通过源码看看线程池是如何实现的:

public void execute(Runnable command) {
	if (command == null)
		throw new NullPointerException();
	
	int c = ctl.get();
	if (workerCountOf(c) < corePoolSize) { // 当前线程池中的线程比corePoolSize少,新建一个线程执行任务
		if (addWorker(command, true))
			return;
		c = ctl.get();
	}
	if (isRunning(c) && workQueue.offer(command)) { // 核心线程数已满,但队列未满,将当前任务添加到队列中
		int recheck = ctl.get();
		// 任务成功添加到队列中后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁了
		if (! isRunning(recheck) && remove(command)) 
			// 如果线程池处于非运行状态,并且把当前的任务从任务队列中移除成功,则拒绝该任务
			reject(command);
		else if (workerCountOf(recheck) == 0) // 如果之前的线程已被销毁完,则新建一个线程
			addWorker(null, false);
	}
	else if (!addWorker(command, false)) // 核心线程已满,队列已满,试着创建一个新的线程
		reject(command); // 如果创建新线程失败了,说明线程池关闭了或者线程池完全满了,拒绝该任务
}

ctl的作用
在线程池中,ctl这个变量贯穿在线程池的整个生命周期中:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

它是一个原子类,主要作用是用来保存线程数量和线程池的状态;我们分析一下上面这段代码,它用到了位运算,一个int数值是32个bit位,这里采用高3位来保存运行状态,低29位来保存线程数量。
上面的方法中调用了ctlOf(int rs, int wc)方法:

private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int RUNNING    = -1 << COUNT_BITS;
private static int ctlOf(int rs, int wc) { return rs | wc; }

其中RUNNING = -1 << count_BITS;也就是-1左移29位,-1的二进制是32个1;那么-1 左移29位也就是[111],所以上面代码中 rs | ws 也就是 111 | 000,得到的结果仍然是111。

同理可以得到其他状态的bit位表示:

private static final int COUNT_BITS = Integer.SIZE - 3;  // 32 - 3 = 29
private static final int CAPACITY   = (1 << COUNT_BITS) - 1; // 将1 的二进制左移29位,再减一表示最大容量  0001 1111 1111 1111 1111 1111 1111 1111

// 运行状态保存在int值的高3位(所有数值左移29位)
private static final int RUNNING    = -1 << COUNT_BITS; // 接收新任务,并执行队列中的任务
private static final int SHUTDOWN   =  0 << COUNT_BITS; // 不接收新任务,但是执行队列中的任务
private static final int STOP       =  1 << COUNT_BITS; // 不接收新任务,不执行队列中的任务,中断正在执行中的任务
private static final int TIDYING    =  2 << COUNT_BITS; // 所有任务都已结束,线程数量为0, 处于该状态的线程池即将调用ternamited()方法
private static final int TERMINATED =  3 << COUNT_BITS; // terminated()方法执行完成

在这里插入图片描述

addWorker方法
如果工作线程数量小于核心线程数的时候,会调用addWorker方法创建一个工作线程;通过源码可以发现,这个方法其实只做了两件事:
1). 通过循环CAS操作将线程数加1
2). 新建一个线程并启动

private boolean addWorker(Runnable firstTask, boolean core) {
	retry:  // goto语句,避免死循环
	for (;;) {
		int c = ctl.get();
		int rs = runStateOf(c);

		// Check if queue empty only if necessary.
		// 1. 线程池已经shutdown后(rs >= shutdown),还要添加新任务,拒绝
		// 2. SHUTDOWN状态不接收新任务,但仍然会执行已经添加到队列中的任务;所以当线程池是SHUTDOWN的时候,
		// 而传进来的任务为空,并且任务队列不为空的时候,是允许添加新线程的,再把这个条件取反,就表示不能添加新的worker了
		if (rs >= SHUTDOWN &&
			! (rs == SHUTDOWN &&
			   firstTask == null &&
			   ! workQueue.isEmpty()))
			return false;

		for (;;) { // 自旋
			int wc = workerCountOf(c); // 获得worker工作线程数
			// 如果工作线程数大于默认容量大小,或者大于核心线程数大小,则直接返回false表示不能添加新的worker
			if (wc >= CAPACITY ||
				wc >= (core ? corePoolSize : maximumPoolSize))
				return false;
			if (compareAndIncrementWorkerCount(c)) // 通过CAS来增加工作线程数,如果失败则直接重试
				break retry;
			c = ctl.get();  // 再次获得ctl的值
			if (runStateOf(c) != rs) // 如果状态不相等, 说明线程池的状态发生了变化,继续重试
				continue retry;
			// else CAS failed due to workerCount change; retry inner loop
		}
	}
	// 上面这段代码主要是对worker的数量做原子+1的操作,下面的代码才是真正的创建一个新的worker
	boolean workerStarted = false; // 工作线程是否启动的标识
	boolean workerAdded = false; // 工作线程是否已经添加成功的标识
	Worker w = null;
	try {
		w = new Worker(firstTask); // 创建一个worker,把当前任务传递给worker
		final Thread t = w.thread; // 从worker对象中取出线程
		if (t != null) {
			final ReentrantLock mainLock = this.mainLock;
			mainLock.lock(); // 获得重入锁,避免并发问题
			try {
				// Recheck while holding lock.
				// Back out on ThreadFactory failure or if
				// shut down before lock acquired.
				int rs = runStateOf(ctl.get());
				// 只有当前线程池是正在运行的状态,或者是SHUTDOWN且firsttask为空,才能添加到workers集合中
				if (rs < SHUTDOWN ||
					(rs == SHUTDOWN && firstTask == null)) {
					if (t.isAlive()) // 任务刚封装到work里面,还未启动,线程不可能是alive状态,如果是alive状态就直接抛异常
						throw new IllegalThreadStateException();
					workers.add(w); // 将新创建的worker添加到workers集合中
					int s = workers.size(); // 获取工作线程的数量
					// 如果集合中工作线程的数量大于最大线程数,则更新最大线程数的值;
					// 这里的最大线程数是指线程池中曾经出现过的最大线程数
					if (s > largestPoolSize)
						largestPoolSize = s;
					workerAdded = true; // 表示工作线程创建成功
				}
			} finally {
				mainLock.unlock();
			}
			if (workerAdded) { // worker添加成功
				t.start(); // 启动线程
				workerStarted = true;
			}
		}
	} finally {
		if (! workerStarted)
			addWorkerFailed(w); // 如果添加失败,递减实际工作线程数
	}
	return workerStarted;
}

工作线程
我们发现addWorker方法只是构造了一个Worker,并且把firstTask封装到worker中,它的作用是什么呢?
Worker类集成了AQS并且实现了Runnable接口;其中包含了firstTask和thread属性,firstTask用来保存传入的任务;thread是在调用构造方法时通过ThreadFactory来创建的线程,是用来处理任务的线程。

在调用构造方法时,需要传入任务,这里通过getThreadFactory().newThread(this)来新建一个线程,newThread方法传入的参数是this,因为worker本身继承了Runnable接口,也就是一个线程,所以一个worker对象在启动的时候会调用Worker类中的run方法。

Worker继承了AQS,使用AQS来实现独占锁的功能,这里问什么不适用ReentrantLock来实现呢?从代码中可以看到tryAcquire方法是不允许重入的,而ReentrantLock是允许重入的。

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
	
	private static final long serialVersionUID = 6138294804551838833L;

	final Thread thread;
	Runnable firstTask;
	volatile long completedTasks;

	Worker(Runnable firstTask) {
		setState(-1); // inhibit interrupts until runWorker
		this.firstTask = firstTask;
		this.thread = getThreadFactory().newThread(this);
	}

	public void run() {
		runWorker(this);
	}

	protected boolean isHeldExclusively() {
		return getState() != 0;
	}

	protected boolean tryAcquire(int unused) {
		if (compareAndSetState(0, 1)) {
			setExclusiveOwnerThread(Thread.currentThread());
			return true;
		}
		return false;
	}

	protected boolean tryRelease(int unused) {
		setExclusiveOwnerThread(null);
		setState(0);
		return true;
	}

	public void lock()        { acquire(1); }
	public boolean tryLock()  { return tryAcquire(1); }
	public void unlock()      { release(1); }
	public boolean isLocked() { return isHeldExclusively(); }

	void interruptIfStarted() {
		Thread t;
		if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
			try {
				t.interrupt();
			} catch (SecurityException ignore) {
			}
		}
	}
}

runWorker方法
前面已经了解到ThreadPoolExecutor的核心方法是addWorker方法,主要作用是增加工作线程。而Worker简单理解就是是一个线程,里面重写了run方法,线程池中真正的处理逻辑也就是调用Worker的run方法,Worker中的run方法又调用ThreadPoolExecutor类中的runWorker方法,这个runWorker方法主要做了下面几件事:

  1. 如果task不为空,则执行task
  2. 如果task为空,则通过getTask()去获取任务,并赋值给task;如果去到的task不为空,则执行
  3. 执行完毕后通过while循环继续调用getTask()取任务
  4. 如果getTask()取到的任务依然为空,那么整个runWorker方法执行完毕
final void runWorker(Worker w) {
	Thread wt = Thread.currentThread();
	Runnable task = w.firstTask;
	w.firstTask = null;
	w.unlock(); // unlock表示当前worker允许中断,在new worker的时候默认的state=-1,这里通过tryRelease方法将state设置成0
	boolean completedAbruptly = true;
	try {
		// 在这个while循环中实现了线程的复用,如果task为空,则通过getTask()来获取任务
		while (task != null || (task = getTask()) != null) {
			w.lock(); // 这里加锁不是为了防止并发执行任务,而是为了在shutdown的时候不终止正在运行的任务
			// 线程池尾stop状态时,不接收新的任务,不执行已经添加到队列中的任务,并且要中断正在执行的任务。
			if ((runStateAtLeast(ctl.get(), STOP) ||
				 (Thread.interrupted() &&
				  runStateAtLeast(ctl.get(), STOP))) &&
				!wt.isInterrupted())
				wt.interrupt();
			try {
				beforeExecute(wt, task); // 这里默认是没有实现的,在某些特定场景我们可以自己重写
				Throwable thrown = null;
				try {
					task.run();
				} catch (RuntimeException x) {
					thrown = x; throw x;
				} catch (Error x) {
					thrown = x; throw x;
				} catch (Throwable x) {
					thrown = x; throw new Error(x);
				} finally {
					afterExecute(task, thrown);
				}
			} finally {
				task = null;
				w.completedTasks++;
				w.unlock();
			}
		}
		completedAbruptly = false;
	} finally {
		processWorkerExit(w, completedAbruptly);
	}
}

线程池的创建
我们可以通过ThreadPoolExecutor来创建一个线程池:

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

创建线程池时需要传入几个参数,如下:

  1. corePoolSize(线程池的基本大小): 当提交一个任务到线程池的时候,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程;等到需要执行的任务数大于线程池基本大小的时候就不再创建。如果调用了线程池的prestartAllCoreThreads()方法, 线程池会提前创建并启动所有线程
  2. maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数,如果队列满了,并且已创建的线程数小于最大线程数,则线程池会创建新的线程执行任务。需要注意的是如果使用了无界的任务队列,这个参数就没什么效果了
  3. keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间;所以如果任务很多,并且每个任务的执行时间很短,则可以调大时间,提高线程利用率
  4. unit(线程活动保持时间的单位):可选的单位有天DAYS、HOURS、MINUTES、MILLISECONDS、MICROSECONDS,NANOSECONDS。
  5. workQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:
    ArrayBlockingQueue:基于数组结构的有界阻塞队列,按照FIFO对元素进行排序
    LinkedBlockingQueue:基于链表结构的阻塞队列,按照FIFO对元素进行排序;吞吐量要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了此队列
    SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用 移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工 厂方法Executors.newCachedThreadPool使用了这个队列。
    PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  6. threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设 置更有意义的名字
  7. handler:当队列和线程池都满了,说明线程池处于饱和状 态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法 处理新任务时抛出异常。Java线程池框架提供了以下4种策略:
    AbortPolicy:直接抛出异常。
    CallerRunsPolicy:只用调用者所在线程来运行任务。
    DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    DiscardPolicy:不处理,丢弃掉。

向线程池提交任务
可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。 通过以下代码可知execute()方法输入的任务是一个Runnable类的实例。

threadsPool.execute(new Runnable() {                       
 	@Override                        
 	public void run() {                                
 		// TODO Auto-generated method stub                        
 	}                
 });

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个 future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方 法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线
程一段时间后立即返回,这时候有可能任务没有执行完。

Future<Object> future = executor.submit(harReturnValuetask); 
try {
	Object s = future.get();
} catch(Exception e) {
	// TODO
} finally {
	executor.shutdown();
}

关闭线程池
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线 程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务 可能永远无法终止。

但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务 都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪 一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭 线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

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

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

CPU密集型任务应配置尽可能小的 线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配 置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务 和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。

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

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

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无法无天过路客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值