写在前面:
本篇博客是仿照了JDK1.8线程池的实现,如对JDK1.8线程池很熟悉。
那么,,,,,就可以离开本页面啦(✪ω✪)
点击下载本篇博客源码
线程池的原理其实很简单,就是不把执行完任务的线程关闭,而是从任务队列中拿到新的任务,接着执行。而实现一个线程池的关键点在于以下几点:任务队列、向线程池提交任务、安全的关闭线程池、什么时候创建线程、什么时候关闭线程以及执行拒绝策略等。
线程池类
public class SuyeThreadPool implements ExecutorService {
/**
* 线程池最高效率执行的工作线程数
*/
protected int bestPoolThreadSize;
/**
* 线程池中活跃的最大工作者线程数量
*/
protected int theMostPoolThreadSize;
/**
* 任务阻塞队列
*/
protected final BlockingQueue<Runnable> taskQueue;
/**
* 存放工作者线程的集合
*/
protected final HashSet<WorkThread> workThreadSet;
/**
* 工作者线程的数量
*/
protected volatile int workThreadSize;
/**
* 保存线程池的状态
*/
protected final SuyeThreadPoolState suyeThreadPoolState;
private final ThreadRepository threadRepository;
//全局锁,用于同步
protected final Lock mainLock=new ReentrantLock();
protected final Condition mainCondition=mainLock.newCondition();
//拒绝策略
protected int rejectStrategy;
}
总类实现了ExecutorService 接口,而上述字段在接下来讲解。
线程池的状态
线程池其实是一个容器,既然是一个容器,那么我们便要有一个状态来表明线程池是处于什么状态。为了简单起见,定义如下的线程池的运行情况:
RUN状态
:大小为0,表明线程池在正常的执行任务时的状态,该状态下可提交任务;STOP状态
:大小为1,表明线程池处于销毁前的状态,该状态下不可以提交任务,但是线程可以接着处理任务队列中的任务;DESTROY状态
:大小为2,该状态下表明线程池已被终止,等待GC回收
线程池的状态不仅包含着线程池的运行情况,同时也应当包含进当前线程池中的线程数量。所以这里我们要在多线程的环境中维持两个共享变量,而维持其安全性,无非就是三种方法:发布一个不变的对象、CAS原子操作、利用锁。发布一个不变对象时行不通的,状态在一直会改变,而利用锁来维持两个变量的安全性,那么也太重量级了。
所以只能用CAS原子操作来实现其功能,但是利用J.U.C包下的原子变量类AtomicInteger
只能维持一个Integer
类型的变量,这里就要充分利用一个int类型的空间了。可以观察到,上述定义的线程池的运行情况只有三种,其数值转化为二进制只占有2位,所以可以让一哥32位的int型变量的最后两位作为存贮线程池运行的情况,其它用作线程池中工作线程的数量(当然,这里可以随意发挥,是情况而定)。用位操作来实现我们的需求,线程池状态管理源码如下:
package suyeq;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* Created with IntelliJ IDEA.
*
* @author: Suyeq
* @date: 2019-03-07
* @time: 20:05
*/
public class SuyeThreadPoolState {
private final int RUN=0;
private final int STOP=1;
private final int DESTROY=2;
private final int poolState=(1<<2)-1;
private final int poolThreadSize=Integer.MAX_VALUE-3;
private final AtomicInteger poolStateAndWorkThreadSize=new AtomicInteger(poolStateMergeSize(RUN,0));
private static SuyeThreadPoolState suyeThreadPoolState;
private SuyeThreadPoolState(){}
/**
* 获得线程池中工作者线程的数量
* @return
*/
public int getWorkThreadSize(){
return (getPoolSizeAndState()&poolThreadSize)>>2;
}
/**
* 获得线程池的状态
* @return
*/
public int getPoolState(){
return getPoolSizeAndState()&poolState;
}
/**
* 获得状态与数量的合并二进制编码
* @param poolState
* @param poolThreadSize
* @return
*/
private int poolStateMergeSize(int poolState,int poolThreadSize){
return poolState | (poolThreadSize<<2);
}
/**
* 让线程池的状态变为STOP
* @return
*/
public boolean setPoolStateToStop(){
return poolStateAndWorkThreadSize.compareAndSet(getPoolSizeAndState(),poolStateMergeSize(STOP,getWorkThreadSize()));
}
/**
* 让线程池的状态变为DESTROY
* @return
*/
public boolean setPoolStateToDestroy(){
return poolStateAndWorkThreadSize.compareAndSet(getPoolSizeAndState(),poolStateMergeSize(DESTROY,getWorkThreadSize()));
}
/**
* 增加工作者线程的数量
* @return
*/
public int increasePoolThreadSize(){
int poolThreadSize=getWorkThreadSize();
int newPoolThreadSize;
//int newPoolThreadSize=poolThreadSize+1;
do {
newPoolThreadSize=getWorkThreadSize()+1;
}while (!poolStateAndWorkThreadSize.compareAndSet(getPoolSizeAndState(),poolStateMergeSize(getPoolState(),newPoolThreadSize)));
//poolStateAndWorkThreadSize.compareAndSet(getPoolSizeAndState(),poolStateMergeSize(getPoolState(),newPoolThreadSize));
return newPoolThreadSize;
}
/**
* 获得单例
* @return
*/
public static SuyeThreadPoolState getInstance(){
synchronized (SuyeThreadPool.class){
if (suyeThreadPoolState==null){
suyeThreadPoolState=new SuyeThreadPoolState();
return suyeThreadPoolState;
}
return suyeThreadPoolState;
}
}
public int getPoolSizeAndState(){
return poolStateAndWorkThreadSize.get();
}
public int RunState(){
return RUN;
}
public int StopState(){
return STOP;
}
public int DestroyState(){
return DESTROY;
}
}
其中对于线程池的运行情况,采取的策略是只试图更新一次,若失败则不更新,因为这时候已经其它线程帮它做了,而对于线程池中工作者线程数量,则采取一直更新,直到更新成功为止,因为可能有不同的线程在增加工作者线程。
工作者线程
对于线程而言,Thread
类无法携带其它的信息,且只能执行一次run
方法,因此我们应当对线程进行封装,使其绑定一个可以不断从任务队列中取出任务的run
方法,其最好的办法,就是在创建该线程时,就将该run
方法绑定该线程,使其运行其绑定的run
方法:
/**
* 封装的工作者线程
*/
class WorkThread implements Runnable {
//线程的首任务
private Runnable firstTask;
//封装的线程
private Thread thread;
//该线程完成的任务数量
private volatile int completeTask=0;
public WorkThread(Runnable firstTask){
this.firstTask=firstTask;
thread=threadRepository.newThread(this);
}
@Override
public void run() {
try {
runWork(this);
} catch (InterruptedException e) {
System.out.println("中断线程了");
e.printStackTrace();
}finally {
reduceWorkThread(this);
System.out.println("处理中断啦");
// System.out.println("重连操作开始");
return;
}
}
}
从代码中可以看出,WorkerThread
类实现了Runnable
接口,并在创建一个新的线程时,将自身作为一个参数传入进去,这样便就能启动线程时运行WorkerThread
类里面的run
方法。firstTask是该线程处理的第一个任务,处理完该任务就会从任务队列中取得任务继续处理。因为是提交了任务才会创建新的线程,所以firstTask会绑定刚开始的线程执行。其中的runWork方法就是从任务队列中不断的拿到任务执行的方法,其实现如下:
/**
* 工作者线程运行
* @param workThread
* @throws InterruptedException
*/
private void runWork(WorkThread workThread) throws InterruptedException {
Thread thread=Thread.currentThread();
Runnable task=workThread.firstTask;
workThread.firstTask=null;
while (task!=null || (task=getTask(workThread,false))!=null){
//workThread.lock.lock();
System.out.println("任务执行中");
task.run();
System.out.println("任务执行完成");
workThread.completeTask++;
task=null;
//workThread.lock.unlock();
if (thread.isInterrupted()){
throw new InterruptedException();
}
}
}
其流程是如果是第一次执行任务,那么就直接运行firstTask
,运行完成就利用getTask
方法从任务队列中取得任务,在while循环
里面继续运行,直到任务队列为空,那么线程关闭退出,当然如果运行的时候发生了中断异常,那么也要主动抛出异常,交由上述run
方法集中处理,在那里决定是处理中断减少线程数量还是从新创建一个新的工作者线程继续执行任务。
任务队列
线程池的任务队列其实是生产者与消费者模式的典型应用,只有少量的生产者提交任务,多数消费者(工作者线程)消耗任务。所以这里应当用一个阻塞队列来实现功能,即可以满足如下场景:
- 写入任务时,读取任务应当阻塞,读读之间不阻塞;
- 当任务队列为空的时候,取出任务应当阻塞等待有任务才取出,而且应当限时等待;
- 当任务队列满额情况时,写入任务应当被阻塞;
上述的getTask方法实现如下:
/**
* 从任务队列中获取任务
* @return
*/
private Runnable getTask(WorkThread workThread,boolean isLimitedTime) throws InterruptedException{
while (true){
if (taskQueue.isEmpty()){
System.out.println("任务队列为空");
reduceWorkThread(workThread);
return null;
}
Runnable task=isLimitedTime ? taskQueue.poll(3,TimeUnit.SECONDS):taskQueue.take();
System.out.println("将要判断任务是否为空");
if (task!=null){
System.out.println("从任务队列中取得线程");
return task;
}
}
}
这里使用了一个isLimitedTime 标记,倘若为真,那么就在任务队列为空的情况下等待3秒钟,否则就会一直等待。
提交任务
向线程池提交任务应当考虑以下几个方面:在什么时候可以创建线程并执行任务,什么时候不创建线程插入任务队列,什么时候执行拒绝任务策略等等。关于这些,先来理解几个概念,核心线程数与最大线程数。核心线程数是指在当前线程数下,当前CPU的利用率是最高的,且线程之间不相互制约,其一般与CPU核数相等,可以用如下方式获取:
Runtime.getRuntime().availableProcessors();
而最大线程数是指线程池规定的最大线程运行的数量,达到此情况下线程之间会互相制约。用工厂工人的方式形象的描述两者的区别:比如工厂中有四台机器,现在有四个工人来操控机器,在此时最为高效的(假如工人没有疲惫),因为每个工人与机器都在工作,这时候工人的数量就是核心线程数,而如果再来几个工人,这时候每个工人间会对着机器轮流操作,轮流的途中会消耗时间,相反可能还没有原来的高效率了,这时候的数量就是最大线程数了。
所以,对于线程池提交任务的策略。定义如下:
- 假如线程池中工作者线程数小于核心线程数,那么就创建一个新的工作者线程来执行任务;
- 假如任务队列为满,且达到了核心线程数,那么便插入任务队列中;
- 假如任务队列已满,且未达到最大线程数,那么便创建新的工作者线程执行任务;
- 假如任务队列已满,且达到了最大线程数,那么便执行拒绝策略;
具体编码如下:
if (command==null){
throw new NullPointerException();
}
int poolThreadSize=suyeThreadPoolState.getWorkThreadSize();
int poolState=suyeThreadPoolState.getPoolState();
int RunState=suyeThreadPoolState.RunState();
//需要判断线程池的状态以及线程数量
if (poolThreadSize<bestPoolThreadSize){
if (addWorkThread(command)){
return;
}
poolState=suyeThreadPoolState.getPoolState();
}
if (poolState==RunState && taskQueue.offer(command)){
System.out.println("插入任务队列,不创建新的线程");
return;
}
poolState=suyeThreadPoolState.getPoolState();
if (poolState==RunState && !taskQueue.offer(command) && (poolThreadSize<theMostPoolThreadSize)){
if (addWorkThread(command)){
System.out.println("任务队列已满,且线程池中工作者线程数量小于最大数量,则创建新的线程");
return;
}
}
//执行拒绝策略
System.out.println("执行拒绝策略");
RejectionStrategy.rejectStrategy(command,rejectStrategy);
return;
增加工作者线程
创建新的工作者线程的情况,在多线程的情况下,应当再次检查线程池与任务队列的改变,当确定真的可以创建时才进入全局锁来创建一个新的工作者线程并启动该线程:
/**
* 增加工作者线程
* @param firstTask
* @return
*/
protected boolean addWorkThread(Runnable firstTask){
// retry:
// for (;;){
/**
* 如果线程池的状态大于Stop或者任务队列为null
* 又或者线程第一个任务为null,那么就拒绝创建新的线程
* 又或者工作者线程数量大于最大工作者线程数量
* ps:这里应当用循环确认情况
*/
int poolState=suyeThreadPoolState.getPoolState();
int poolThreadSize=suyeThreadPoolState.getWorkThreadSize();
System.out.println("poolThreadSize:"+poolThreadSize);
System.out.println("poolState:"+poolState);
if (taskQueue.isEmpty() && firstTask==null){
return false;
}else if (poolState>=suyeThreadPoolState.StopState()){
return false;
}else if(poolThreadSize>=theMostPoolThreadSize){
return false;
}
// else {
// break retry;
// }
// }
WorkThread workThread=new WorkThread(firstTask);
if (workThread!=null){
final Thread thread=workThread.thread;
/**
* 防止在其他线程中提交任务
* 因而需要加锁
*/
this.mainLock.lock();
if (thread.isAlive()){
throw new IllegalStateException();
}
workThreadSet.add(workThread);
workThreadSize=suyeThreadPoolState.increasePoolThreadSize();
this.mainLock.unlock();
thread.start();
if (!thread.isAlive()){
//创建线程失败情况
}
}
return true;
}
线程池的拒绝策略
拒绝策略可以由自己任意发挥,比如加入一个集合中,等待线程池任务队列不满的情况插入队列等等,但是在这里简单起见,就定义了如下策略:
public class RejectionStrategy {
/**
* 拒绝掉任务,并抛出异常(默认)
*/
public final static int ABANDONED=1;
/**
* 由调用者线程执行该任务
*/
public final static int CALLER=2;
private static void abandoned() {
try {
throw new OutOfTaskQueueException();
} catch (OutOfTaskQueueException e) {
e.printStackTrace();
}
}
private static void caller(Runnable task){
task.run();
}
public static void rejectStrategy(Runnable task,int rejectChoose){
if (rejectChoose==ABANDONED){
abandoned();
}else if (rejectChoose==CALLER){
caller(task);
}
}
}
其它提交方法
因为FutureTask
实现了Runnable
接口,就可以用FutureTask
封装提交任务的run
方法,来实现有结果的提交方法。
/**
* 指定返回结果的提交方法
* @param task
* @param result
* @param <T>
* @return
*/
@Override
public <T>Future<T> submit(final Runnable task, final T result) {
FutureTask<T> future=new FutureTask<T>(new Callable<T>() {
@Override
public T call() throws Exception {
task.run();
return result;
}
});
execute(future);
return future;
}
到这里整个线程池算是实现完成了,线程池虽然原理简单,但是其具体的实现需要仔细慎重考虑,因为在多线程的环境中要考虑的因素太多了,总之,在我们编码的时候需要谨慎考虑各种情况,这样写出来的代码才不会在后期改动很大。