从0开始实现一个线程池(对标java jdk ThreadPoolExecutor)

关于线程池的基础知识,参考:java线程池ThreadPoolExecutor的原理及使用
这篇文章从实现的角度来实现两个版本的线程池,并对比java jdk中的ThreadPoolExecutor,看一下与工业级线程池的差距。

关于线程池的四个思考

  1. 线程池刚刚创立,还没有Task到来的时候,池中的线程处于什么状态?
    回答:如果是Runnable状态,那么cpu就可以调度其进行运行了,所以不能是就绪状态。
    也不能是TIMED_WAITING状态,是处于waiting状态的,等待任务
  2. 当Task到来的时候,线程池中的线程如何得到通知?
    回答:线程池中有一个阻塞队列,有任务放入阻塞队列,会执行notify,于是在锁池中的线程被唤醒,进行执行
  3. 当线程池中的线程完成工作,如何回到池中?
    回答:实际上线程是去阻塞队列中取任务,如果没有任务,就再次进入waiting状态
  4. Task是什么东西?
    回答:Task是自己定义的一个数据结构,不一定是Runnable对象。只要这个Task能够被你的线程识别就可以,在Task中要执行具体的业务。

实际上这4个问题是相互关联的,这四个问题就构成了线程池的实现思路,其关键就是要有一个阻塞队列。
在这里插入图片描述
左边可能有一大堆线程向里面put,右边是线程池中的线程从中取数据。

当6个格子已经满了,那么左侧的线程只能阻塞了

第一版线程池

Task接口

public interface Task{
    public void execute();
}

ThreadPool

public class ThreadPool{
    // 要持有一个阻塞队列
    private BlockingQueue taskQueue = null;
    // 持有一系列的线程
    private List<WorkerThread> threads = new ArrayList<>();
    // 是否停止线程池
    private boolean isStopped = false;
    
    public ThreadPool(int numOfThreads, int maxNumOfTasks){
        taskQueue = new BlockingQueue(maxNumOfTasks);
        for(int i = 0; i<numOfThreads; i++){
            // 创建线程
            threads.add(new WorkerThread(taskQueue));
        }
        for(WorkerThread thread: threads){
            // 启动线程 等待调度 为Runnable状态
            thread.start();
        }
    }  
    
    public synchronized void execute(Task task) throws Exception{
        if(this.isStopped){
            throw new IllegalStateException("ThreadPool is stopped");
        }
        // enqueue放入task到blockingqueue中
        this.taskQueue.enqueue(task);
    }
    
    public synchronized void stop(){
        this.isStopped = true;
        for(WorkerThread thread: threads){
            thread.doStop();
        }
    }
    
}

WorkerThread

public class WorkerThread extends Thread{
    private BlockingQueue taskQueue = null;
    // 这个线程是否停止
    private boolean isStopped = false;
    
    public WorkThread(BlockingQueue queue){
        // 比如现在阻塞队列中来了一个任务,线程1被唤醒,去执行,执行完成后,再继续while循环中,如果有值,就执行,否则就进入阻塞状态
        while(!isStopped()){
            try{
                // Runnable --> Waiting,当前线程就会被阻塞
                // 比如上面启动了5个线程,5个线程执行到这里,就都进入了阻塞状态
                Task task = (Task)taskQueue.dequeue();             		  
                task.execute();
            }catch(Exception e){
                // log or otherwise report exception
                // but keep pool thread alive
            }
        }
    }     
    
    public synchronized void doStop(){
        isStopped = true;
        // break pool thread out of dequeue() call
        this.interrupt();
    }
    
    public synchronized boolean isStopped(){
        return isStopped;
    }
}

关键的代码

taskQueue.dequeue(); 于是当前线程就会被阻塞

比如上面启动了5个线程,5个线程执行到这里,就都进入了阻塞状态。

比如现在阻塞队列中来了一个任务,线程1被唤醒,去执行,执行完成后,再继续while循环中,如果有值,就执行,否则就进入阻塞状态

线程池停止

通过interrupt打断waiting,在execute方法中是无法打断的

下一次while循环的时候,发现isStop=true,就停止了,所以线程池停止是会把当前的任务都执行完成之后再停止。

JDK线程池中的实现1:核心执行流程

我们看一下ThreadPoolExecutor中实现的核心执行流程

public class ThreadPoolExecutor{
    
    private final BlockingQueue<Runnable> workQueue;
    
    private final ReentrantLock mainLock = new ReentrantLock();
    
    private final HashSet<Worker> workers = new HashSet<Worker>();
    
    private final Condition termination = mainLock.newCondition();
    
    public void execute(Runnable command){
        // 这里涉及到corePoolSize maxPoolSize的逻辑 为了和上面做对比,这块先省略
        addWorder(command);
    }
    
    private boolean addWorker(Runnable firstTask){
        Worker w = null;
        w = new Worker(firstTask);
        final Thread t = w.thread;
        workers.add(w);
        // 这里会启动线程,进入到Runnable状态
        t.start();
    }
    
    public void tryTerminate(){
        
        termination.signalAll();
    }
    
    
    private final class Woker implements Runnable{
        
        final Thread thread;
        
        Runnable firstTask;
        
        Worker(Runnable firstTask){
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
                
        public void run(){
            // delegate runWorker method
            runWoker(this);
        }
        
        final woid runWorker(Worker w){
            Thread wt = Thread.currentThread();
            Runnable task = w.firstTask;
            // while循环 等getTask()==null的时候,其就会进入到waiting状态
            while(task!=null || (task=getTask())!=null){
                task.run();   
            }
        }
        
        private Runnable getTask(){
            Runnable r = timed?
                workQueue.poll() : workQueue.take();
        }
    }
}

当执行

threadExecutor.execute(new LiftOffRunnable());
threadExecutor.execute(new LiftOnRunnable());

其执行流程是:

  1. 执行execute方法,会创建Worker,Worker class中含有两个成员变量Thread thread和Runnable firstTask,将创建的Worker实例加入到blockingQueue中,将thread启动

  2. thread启动后,执行run方法,delegate to runWorker方法,其在一个while循环中获取queue中的Worker实例,获取到了,就执行run方法,如果没有获取到,就进入waiting状态

可以看到这两者的核心思路是相同的。

第二版线程池

参考:gitee链接

这是一个相对完整的实现,包括coreThreadSize,maxThreadSize,任务拒绝策略,keepalive时间等

一个完整的线程池应该具备:

  1. 任务队列,用来缓存提交的任务,LinkedRunnableQueue

  2. queueSize,任务队列主要存放提交的Runnable,为了防止内存溢出,需要限制任务数量

  3. 线程数量管理,init->core->max

  4. 任务拒绝策略

  5. 线程工厂:用于个性化定制线程,比如将线程设置为守护线程以及设置线程名称

  6. keepalive时间:该时间主要决定线程各个重要参数,如initSize,coreSize,maxSize,自动维护线程池线程数量的采样时间间隔

类图
在这里插入图片描述
这里面线程池使用者提交的是Runnable,但是在线程池内部使用的是InternalTask,这个地方有些绕

  1. 创建BasicThreadPoolTask,wangwenjun.threadpool.BasicThreadPoolTask#init中其会启动自身线程,并且启动initSize的两个线程my-thread-pool-exec-0及exec-1

    public BasicThreadPoolTask(int initSize, int maxSize, int coreSize,
                               int queueSize) {
        this.init();
    }
    
    private void init() {
        // 将自身线程启动起来
        start();
    }
    
  2. 在启动两个线程的时候,wangwenjun.threadpool.BasicThreadPoolTask#newThread中,实际上BasicThreadPoolTask 线程池中 每个线程执行的任务都是InternalTask,然后把thread和InternalTask 组合成为ThreadTaskWrapper对象,将这个ThreadTaskWrapper 放到threadQueue这个ArrayDeque里面

  3. 此时两个启动的线程就都执行了InternalTask的run方法,阻塞在runnableQueue.take()这里

    为什么要使用InternalTask?原因就在这里,如果BasicThreadPoolTask持有的LinkedRunnableQueue中放的都是Runnable,那么线程去取的时候就直接执行完成了(调用者线程是不知道queue存在的,所以没办法再调用者线程中做阻塞),没办法做阻塞了,所以需要InternalTask来实现线程阻塞

  4. 当线程池使用者提交了Runnable,其执行wangwenjun.threadpool.BasicThreadPoolTask#execute方法,调用LinkedRunnableQueue#offer方法,而此时阻塞在runnableQueue.take()的线程满足条件,开始执行Runnable的run方法

  5. 当销毁线程池的时候,需要

    threadTask.internalTask.stop();
    threadTask.thread.interrupt();
    

    为了支持这两个操作 需要持有这两个对象,于是采用了ThreadTask,并且使用了ArrayDeque数据结构

BasicThreadPoolTask内部结构总结
在这里插入图片描述
这里面实现了线程池中工作线程数量的扩容和缩容

使用了jvisualvm来查看线程状态

下图是线程池刚刚启用的时候,由initSize 2 threads -->coreSize 4 threads

thread完成任务的时间是5秒,打印采样的间隔也是5秒,维护线程池线程数量的采样间隔是10秒

所以28:16秒创建两个线程,执行两个任务ThreadTask

28:21,由于线程池线程数量的采样间隔是10秒,所以仍旧是2个线程,执行两个任务,所以剩下的任务数量是16个

而28:26,其扩容了线程池,于是4个线程取到了4个任务

在这里插入图片描述

28:31,应该就剩下12个任务了

28:36,应该就剩下8个任务了,并且再度扩容,4个线程扩容到6个线程

在这里插入图片描述

28:41,应该就剩下两个任务了,6个线程中pool-3和pool-2抢到了这两个任务

28:46,所有任务完成,此时又进行线程数量检测,线程池缩容

private final Queue<ThreadTask> threadQueue = new ArrayDeque<>();

这里选择的数据结构是ArrayDeque

    public E remove() {
        return removeFirst();
    }

由于pool-0和pool-1线程都阻塞在take()处,所以需要interrupt

"thread-pool--1" #32 prio=5 os_prio=0 tid=0x0000000020d34800 nid=0x3b18 in Object.wait() [0x00000000254ee000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at wangwenjun.threadpool.LinkedRunnableQueue.take(LinkedRunnableQueue.java:43)
	- locked <0x000000076be3ec70> (a java.util.LinkedList)
	at wangwenjun.threadpool.InternalTask.run(InternalTask.java:25)
	at java.lang.Thread.run(Thread.java:748)

关闭掉这两个线程之后,线程数量=4=coreSize

在这里插入图片描述

最后调用shutdown,先关闭掉线程池中所有的线程,最后把BasicThreadPoolTask也关闭掉

在这里插入图片描述

JDK线程池中的实现2:工作线程数量的扩容和缩容

JDK中的ThreadPoolExecutor,关于keepaliveTime以及线程数量的检测,其并没有如我们第二版设计的方式,将自身设置为一个线程来进行检测和操作

扩容

ThreadPoolExecutor#execute方法

 int c = ctl.get();
        if (workerCountOf(c) < 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);

工作线程数量扩容过程总结
在这里插入图片描述
workQueue是一个BlockingQueue,也就是说当前线程数小于corePoolSize,那么就启用新的线程,如果任务数大于corePoolSize,那么就放到这个workQueue里面(workQueue.offer)。如果workQueue已经满了,那么就启动新的线程,直到maxPoolSize,如果还有新的任务过来,就执行reject policy

对比:

缩容

是在java.util.concurrent.ThreadPoolExecutor#runWorker的时候

try{
    while (task != null || (task = getTask()) != null) {
        task.run();
    }
}        
finally {
    processWorkerExit(w, completedAbruptly);
}

在getTask()的时候,其使用keepalive参数,通过queue.poll(),即如果某个线程没有在keepAliveTime时间内返回为null,那么其就不会执行到processWorkerExit方法

但如果超时了仍然两手空空没拉到活,表明这个临时线程太闲了,这个线程会被销毁回收。

Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();

通过processWorkerExit方法来回收线程

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    workers.remove(w);
}

JDK线程池中的实现3:关闭线程池

shutdown方法

java.util.concurrent.ThreadPoolExecutor#shutdown

interruptIdleWorkers();

java.util.concurrent.ThreadPoolExecutor#interruptIdleWorkers(boolean)

for (Worker w : workers) {
    Thread t = w.thread;
    if (!t.isInterrupted() && w.tryLock()) {
        try {
            t.interrupt();
        }
    }
}

shutdown(); 是一个非阻塞方法

  1. 停止接收外部submit的任务
  2. 内部正在跑的任务和队列里等待的任务,会执行完
  3. interruptIdleWorkers 打断idle threads
  4. 当正在执行的线程变为idle状态,才真正停止

awaitTermination方法

如果想阻塞,在线程池任务结束之后执行某些代码,可以使用awaitTermination方法

Blocks until all tasks have completed execution after a shutdown request, or the timeout occurs, or the current thread is interrupted, whichever happens first.

当前线程阻塞,直到

  • 等所有已提交的任务(包括正在跑的和队列中等待的)执行完
  • 或者等超时时间到
  • 或者线程被中断,抛出InterruptedException

当线程正在执行的时候,其就会block住,从下面的代码可以看出,其timeout的作用仅仅是看是否在规定的时间内完成了所有线程都为Terminated状态,如果有,则返回true,否则返回false

            for (;;) {
                if (runStateAtLeast(ctl.get(), TERMINATED))
                    return true;
                if (nanos <= 0)
                    return false;
                nanos = termination.awaitNanos(nanos);
            }

下面这种使用方式,对于所执行线程可以被打断是有效的

executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.HOURS);
// executorService all worker threads exit
// do something;

setDaemon+awaitTermination

如果不调用shutDown方法,由于ThreadPoolExecutor中有非daemon的thread(默认ThreadFactory daemon = false),所以方法不会执行结束,jvm不会退出

如果所执行不可以被打断(如访问db或者访问网络,很慢,但是无法中断),那么怎么在规定时间结束线程池呢?
如果想使用awaitTermination方法,就是在规定的时间完成,可以通过ThreadPoolExecutor的ThreadFactory setDaemon来实现

shutdownNow方法

shutdownNow(); 也是一个非阻塞线程

  1. 先停止接收外部提交的任务
  2. interrupt all workers thread 尝试将正在跑的任务interrupt中断(测试代码因为是sleep方法,支持中断,如果是不支持中断的,如访问db或者访问网络,很慢,但是无法中断)
  3. drainQueue方法 return list<Runnable> in queue(忽略队列里面等待的任务),可以返回未执行的任务列表
  4. 线程池中所有线程退出

停止线程池测试代码参考:gitee链接

JDK线程池中的实现4:任务拒绝策略

  • java.util.concurrent.ThreadPoolExecutor.DiscardPolicy

    A handler for rejected tasks that silently discards the rejected task. 静默丢弃,根本不知道发生了什么

  • java.util.concurrent.ThreadPoolExecutor.AbortPolicy

    A handler for rejected tasks that throws a {@code RejectedExecutionException}. 抛出异常

  • java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy

    A handler for rejected tasks that runs the rejected task directly in the calling thread of the {@code execute} method, unless the executor has been shut down, in which case the task is discarded.

    比如tomcat线程调用线程池,被拒绝后直接使用tomcat线程来执行

  • java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy

    A handler for rejected tasks that discards the oldest unhandled request and then retries {@code execute}, unless the executor is shut down, in which case the task is discarded.

    处理被拒绝任务的处理程序,它丢弃最旧的未处理请求,,建议不要使用,根本不知道发生了什么

线程执行中RuntimeException错误处理

executorService#execute中发生了错误RuntimeException,如何处理?

注意execute runnable是没有返回值的

方法1:自定义ThreadFactory使用uncaughtExceptionHandler
但是这种方式并不推荐,因为只能拿到thread和throwable cause

但是拿不到Runnable,所以比如失败之后你想做一些补偿,比如再试一次。

或者成功的话,更新数据库某个字段;如果失败了,同样更新数据库某个字段,其拿不到Runnable中的信息。

方法2:采用thread monitor 模式来处理
将任务代码和监控代码写在了一起

方法3:自定义ThreadPoolExecutor

参考:gitee链接

我的思考,对线程池任务是否需要持久化处理?

比如说我们重启服务对线程池的影响。

场景1:kill pid,此时要关闭线程池,

使用shutdown,非阻塞的,不好。

shutdown+awaitTermination指定时间,也可能有失败。

shutdownNow,要丢弃队列中的任务,也不好吧。

是不是需要把未执行完成以及workQueue中丢弃的任务记录下来,否则可能会出现一些不一致的情况。

场景2:kill -9 pid,此时线程池任务以及未执行完成的任务就全部都丢失了,内核来清理资源了。

场景3:线程执行过程中发生RuntimeException,怎么办?

线程池设计上使用了命令模式

比如我们自己创建Thread对象来执行Runnable,就是没有使用命令模式

在java多线程中,可以使用Executor来代替显式地创建Thread对象,这就是使用了命令模式
在这里插入图片描述
这里的Command模式解析

HashSet<Worker>就类似于执行者集合,execute方法就是发出执行的命令,这里的执行命令是借助于blockingQueue自动实现的,最终调用runWorker方法

命令的执行者Receiver是Worker

这里并不是Command关联Receiver,而是Receiver中持有了Command,来进行执行

把Runnable当做是Command,LiftOffRunnable和LiftOnRunnable作为Command的实现

就是把request(LiftOffRunnable和LiftOnRunnable)封装起来(用worker),把调用者(ExecutorService)和执行者(Worker中的thread)进行解耦

当我们调用时,执行的时序首先是ThreadPool(Invoker类),然后是Worker类(Receiver类),最后是Concrete Command类。也就是说一条命令的执行被分成了三步,它的耦合度要比把所有的操作都封装到一个类中要低的多(直接使用一个Thread即Receiver来执行Concrete Command),而这也正是命令模式的精髓所在:把命令的调用者Invoker与执行者Receiver分开,使双方不必关心对方是如何操作的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值