Bootstrap

理解线程池原理--手写一个线程池

理解线程池原理 - - 手写一个线程池

实现思路

用户创建出线程池对象,自定义线程池的核心线程数阻塞队列大小拒绝策略

  • 线程池创建核心线程执行用户提交的任务
  • 当线程池中的核心线程都处于非空闲状态时,用户提交的任务则会进入阻塞队列中等待空闲线程执行
  • 当阻塞队列已满时,后面提交的任务都会采取相应的拒绝策略

本文只是简单的模拟线程池的实现,舍去了原本线程池中最大线程数、keepAliveTime等实现。

具体实现

阻塞队列

阻塞队列的实现其实就是根据生产者消费者模型来实现的,这里使用了双向链表Deque实现,遵循队列的FIFO原则,下面我们模拟了一个线程安全的阻塞队列。

public class BlockQueue<T> {
    private static final Log LOG = LogFactory.get();

    /**
     * 双向链表
     */
    private Deque<T> deque = new ArrayDeque<>();

    /**
     * 队列长度
     */
    private int queueSize;

    /**
     * 锁
     */
    private ReentrantLock lock = new ReentrantLock();

    /**
     * 生产者条件变量(等待队列)
     */
    private Condition fullWaitSet = lock.newCondition();

    /**
     * 消费者条件变量(等待队列)
     */
    private Condition emptyWaitSet = lock.newCondition();

    public BlockQueue(int queueSize) {
        this.queueSize = queueSize;
    }

    /**
     * 存放一个对象进队列中,阻塞式
     * @param t
     */
    public void put(T t) {
        lock.lock();
        try {
            // 当队列为满时
            while (deque.size() >= queueSize) {
                try {
                    // 进入等待队列 阻塞等待
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // push
            LOG.debug("添加阻塞队列: {}", t);
            deque.add(t);
            // 唤醒poll等待的线程
            emptyWaitSet.signalAll();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 尝试存放一个对象进队列中,成功返回true,否则返回false
     * @param t
     * @return boolean
     */
    public boolean tryPut(T t) {
        lock.lock();
        try {
            // 当队列为满时
            if (deque.size() >= queueSize) {
                return false;
            }
            // push
            LOG.debug("添加阻塞队列: {}", t);
            deque.add(t);
            // 唤醒poll等待的线程
            emptyWaitSet.signalAll();
            return true;
        } finally {
            lock.unlock();
        }
    }
    
    /**
     * 从队列中取出一个对象,超时退出
     * @param timeout 超时时间
     * @param timeUnit 时间单位
     * @return
     */
    public T poll(long timeout, TimeUnit timeUnit) {
        // 转换时间为纳秒
        long waitTime = timeUnit.toNanos(timeout);
        lock.lock();
        try {
            // 队列为空时
            while (deque.isEmpty()) {
                if (waitTime <= 0) {
                    return null;
                }
                // 进入等待队列
                try {
                    // 改方法返回 剩余等待的时间
                    // 如果不这样操作 假设用户传递超时时间为2秒,该方法在等待1秒后被唤醒,那么可能下一轮在进入while循环,就还要在等待2秒钟
                    waitTime= emptyWaitSet.awaitNanos(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // poll元素
            T t = deque.removeFirst();
            // 唤醒push等待的线程
            fullWaitSet.signalAll();
            return t;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 从队列中取出一个对象
     * @return obj
     */
    public T take() {
        lock.lock();
        try {
            // 队列为空时
            while (deque.isEmpty()) {
                try {
                    // 进入等待队列 阻塞等待
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // poll元素
            T t = deque.removeFirst();
            // 唤醒push等待的线程
            fullWaitSet.signalAll();
            return t;
        } finally {
            lock.unlock();
        }
    }
}
  • 上述的put、take方法都为阻塞式方法,即当队列为满时,调用put方法会一直阻塞,知道队列中可用;当队列为空时,调用take方法会一直阻塞,知道队列不为空。

  • poll方法需要传入时间参数,即在指定时间内获取不到队列中元素,则 return null,不会一直阻塞等待。

    注意,这里awaitNanos方法返回的是剩余等待的时间,例如传递时间参数为3秒,该方法等待2秒就被唤醒了,则返回值为1秒。

  • tryPut方法,即尝试存放一个对象进阻塞队列中,该方法不会阻塞,立即返回boolean值结果。

线程池

public class ThreadPool {
    private static final Log LOG = LogFactory.get();

    /**
     * 阻塞队列
     */
    private BlockQueue<Runnable> taskQueue;

    /**
     * 线程集合
     */
    private HashSet<Worker> workers = new HashSet<>();

    /**
     * 核心线程数
     */
    private int corePoolSize;

    /**
     * 获取任务时的超时时间
     */
    private long timeout;

    /**
     * 时间单位
     */
    private TimeUnit timeUnit;

    /**
     * 拒绝策略
     */
    private final RejectStrategy<Runnable> rejectStrategy;

    public ThreadPool(int corePoolSize, int queueCapacity, long timeout, TimeUnit timeUnit, RejectStrategy<Runnable> rejectStrategy) {
        this.corePoolSize = corePoolSize;
        this.taskQueue = new BlockQueue<>(queueCapacity);
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.rejectStrategy = rejectStrategy;
    }

    public void execute(Runnable task) {
        synchronized (workers) {
            if(workers.size() < corePoolSize) {
                Worker worker = new Worker(task);
                LOG.debug("创建worker线程: {}", worker.getName());
                workers.add(worker);
                worker.start();
            } else {
                // 尝试加入阻塞队列
                boolean flag = taskQueue.tryPut(task);
                if (!flag) {
                    // 采取拒绝策略
                    this.rejectStrategy.reject(taskQueue, task);
                }
            }
        }
    }

    class Worker extends Thread {
        private Runnable task;

        public Worker(Runnable task) {
            this.task = task;
        }

        @Override
        public void run() {
            while (task != null || (task = taskQueue.take()) != null) {
//            while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
                try {
                    LOG.debug("worker线程: {}, 正在运行task: {}", Thread.currentThread().getName(), task);
                    task.run();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    task = null;
                }
            }
            synchronized (workers) {
                LOG.debug("删除worker: {}", this.getName());
                workers.remove(this);
            }
        }
    }
}
  • 这里重点就是execute方法,接收用户需要执行的任务,如果当前worker线程数(核心线程数)未超过corePoolSize,则创建出Worker对象,执行用户提交的任务。如果当前worker线程数已经达到了corePoolSize大小,则调用tryPut方法尝试将该任务加入到阻塞队列中,如果队列已满,则调用reject方法,采取相应的拒绝策略。

  • 这里的Worker对象其实就是作为线程池中的一个线程,它继承了Thread对象,重写了run方法。在其run方法中,正常情况下,第一轮while循环时,task即为用户需要执行的任务,直接调用了task的run方法,即表示使用当前worker线程去执行用户需要执行的任务,执行完毕后,置task为null,接下来的每一次循环,都会去阻塞队列中获取等待执行任务,继续使用当前的worker线程执行

    这里强调一下start和run方法的区别:

    • start:调用该方法,jvm会重新创建出一个新的线程执行run方法中的代码。
    • run:相当于普通方法的调用,使用当前调用该方法的线程去执行run方法。
    • 这里如果使用take阻塞式方法获取任务,当队列为空时,该worker线程会一直处于waiting等待状态,永远不会结束。
    • 我们也可以使用poll方法从队列中获取任务,指定超时时间,如果在该时间内依旧获取不到队列中的元素时,那就不一直等了,直接跳出while循环,从线程集合中删除该worker线程。
  • 关于一个问题:线程池中的核心线程会不会被回收?

    在实际的线程池中,核心线程的回收由allowCoreThreadTimeOut参数来控制,默认为false,即不会被收。当设置为true时,不管是核心线程还是非核心线程,只要其空闲时间达到指定的KeepAliveTime时,都会被回收。

拒绝策略

在实际的线程池中,拒绝策略被定义为多种,比如抛出异常、死等、记录日志、让调用者自己执行等等。因此,在实现过程中,我们可以通过实现接口的方式,自定义多种不同的拒绝策略。

@FunctionalInterface	// 函数式接口
interface RejectStrategy<T> {
    /**
     * 拒绝策略具体实现
     * @param queue 阻塞队列
     * @param task 任务
     */
    void reject(BlockQueue<T> queue, T task);
}

测试

创建一个线程池,参数如下:

  • 核心线程数为1
  • 阻塞队列大小为1
  • 超时获取元素时间为2秒
  • 拒绝策略:记录日志

假设有3个任务交给该线程池执行,每个任务执行耗时3秒钟。

public static void main(String[] args) {
    final Log log = LogFactory.get();
    ThreadPool threadPool = new ThreadPool(1, 1, 2, TimeUnit.SECONDS, (queue, task) -> {
        // 拒绝策略:

        // 1. 记录日志
        log.debug("task: {}, 未能加入阻塞队列", task);
        // 2. 抛出异常
        //            throw new RuntimeException("队列已满, 任务执行失败: " + task);
        // 3. 让调用者自己执行任务
        //            task.run();
        // .......
    });
    for (int i = 0; i < 3; i++) {
        threadPool.execute(() -> {
            try {
                // 假设每个任务执行时间为3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

使用take方法阻塞式获取,output:

[2023-02-22 14:00:27] [DEBUG] com.ao.demo8.ThreadPool: 创建worker线程: Thread-0
[2023-02-22 14:00:27] [DEBUG] com.ao.demo8.BlockQueue: 添加阻塞队列: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87
[2023-02-22 14:00:27] [DEBUG] com.ao.demo8.ThreadPool: worker线程: Thread-0, 正在运行task: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87
[2023-02-22 14:00:27] [DEBUG] com.ao.demo8.Demo01: task: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87, 未能加入阻塞队列
[2023-02-22 14:00:30] [DEBUG] com.ao.demo8.ThreadPool: worker线程: Thread-0, 正在运行task: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87

即在处理完任务之后,worker线程会一直处于等待状态,程序一直运行。

使用poll方法指定获取时间,output:

[2023-02-22 14:08:00] [DEBUG] com.ao.demo8.ThreadPool: 创建worker线程: Thread-0
[2023-02-22 14:08:00] [DEBUG] com.ao.demo8.BlockQueue: 添加阻塞队列: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87
[2023-02-22 14:08:00] [DEBUG] com.ao.demo8.ThreadPool: worker线程: Thread-0, 正在运行task: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87
[2023-02-22 14:08:00] [DEBUG] com.ao.demo8.Demo01: task: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87, 未能加入阻塞队列
[2023-02-22 14:08:03] [DEBUG] com.ao.demo8.ThreadPool: worker线程: Thread-0, 正在运行task: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87
[2023-02-22 14:08:08] [DEBUG] com.ao.demo8.ThreadPool: 删除worker: Thread-0

线程在处理完包括队列中的任务时,2秒过后,发现未能从队列中获取到新的任务,则不会继续等待,直接释放该worker线程,程序停止运行。

具体流程如下:

Task01 Task02 Task03 ThreadPool Worker01 Blocking Queue RejectStrategy 调用execute() 创建Worker线程-->worker01 执行Task01 调用execute() worker01正在运行,没有空闲的线程 尝试将Task02放入阻塞队列,成功 调用execute() worker01正在运行,没有空闲的线程 尝试将Task03放入阻塞队列,失败 Task03被采取拒绝策略 Task01执行完毕 worker01继续从阻塞队列中获取任务执行 获取到Task02 执行Task02 Task02执行完毕 继续从阻塞队列中获取任务 2秒之后,未获取到任务,释放worker01,结束执行 Task01 Task02 Task03 ThreadPool Worker01 Blocking Queue RejectStrategy

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;