10、教你一步一步如何手写一个简单的线程池

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


线程池模型架构,如下图所示
在这里插入图片描述
图中描绘的是,三个消费线程或者说是核心线程 t1、t2、t3 通过poll方法从阻塞队列中执行任务,主线程不断地往阻塞队列中put任务task,如果核心线程处于忙碌状态,task就放进阻塞队列中

我们用代码实现一个简单线程池

步骤1:自定义拒绝策略接口

@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
 void reject(BlockingQueue<T> queue, T task);
}

这是一个函数式接口,核心线程忙(用完了)同时任务队列也满了时,任务的生产者(main线程)怎么做:死等、 带超时等待、让调用者放弃任务执行、 让调用者抛出异常、 让调用者自己执行任务等等,如果对应一个处理逻辑就创建一个方法的话,太麻烦,因此这块逻辑用函数式接口,传进去的是什么方法,就做什么行为。

这里我们是mian线程的行为,因为后面的测试中,main线程是我们的任务调度者

步骤2:自定义任务队列


@Slf4j(topic = "c.BlockingQueue")
 class BlockingQueue<T> {
    // 1. 任务队列
    private Deque<T> queue = new ArrayDeque<>();
    // 2. 锁
    private ReentrantLock lock = new ReentrantLock();
    // 3. 生产者条件变量
    private Condition fullWaitSet = lock.newCondition();
    // 4. 消费者条件变量
    private Condition emptyWaitSet = lock.newCondition();
    // 5. 容量
    private int capcity;

    public BlockingQueue(int capcity) {
        this.capcity = capcity;
    }

    // 带超时阻塞获取
    public T poll(long timeout, TimeUnit unit) {
        lock.lock();
        try {
            // 将 timeout 统一转换为 纳秒
            long nanos = unit.toNanos(timeout);
            while (queue.isEmpty()) {
                try {
                    // 返回值是剩余时间
                    if (nanos <= 0) {
                        return null;
                    }
                    nanos = emptyWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }
    // 阻塞获取
    public T take() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                try {
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }
    // 阻塞添加
    public void put(T task) {
        lock.lock();
        try {
            while (queue.size() == capcity) {
                try {
                    log.debug("等待加入任务队列 {} ...", task);
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            queue.addLast(task);
            emptyWaitSet.signal();
        } finally {
            lock.unlock();
        }
    }
    // 带超时时间阻塞添加
    public boolean offer(T task, long timeout, TimeUnit timeUnit) {
        lock.lock();
        try {
            long nanos = timeUnit.toNanos(timeout);
            while (queue.size() == capcity) {
                try {
                    if(nanos <= 0) {
                        return false;
                    }
                    log.debug("等待加入任务队列 {} ...", task);
                    nanos = fullWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            queue.addLast(task);
            emptyWaitSet.signal();
            return true;
        } finally {
            lock.unlock();
        }
    }
    public int size() {
        lock.lock();
        try {
            return queue.size();
        } finally {
            lock.unlock();
        }
    }
    public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
        lock.lock();
        try {
            // 判断队列是否满
            if(queue.size() == capcity) {
                rejectPolicy.reject(this, task);
            } else { // 有空闲
                log.debug("加入任务队列 {}", task);
                queue.addLast(task);
                emptyWaitSet.signal();
            }
        } finally {
            lock.unlock();
        }
    }
}

我们从上到下对代码进行一个讲解:

  • queue :生产者创建地任务都放在queue ,Deque是一个双向链表,比LinkedList效率要高,当然这里也可以用LinkedList
  • lock :这里锁用的是ReentrantLock 锁,选用ReentrantLock 的原因,是因为它可以提供两个条件变量(集合)——fullWaitSet 和emptyWaitSet 。
  • fullWaitSet :当任务队列queue满的时候,生产者线程(对应我们上图的main线程)就不能再生产了,而要进入fullWaitSet 阻塞。
  • emptyWaitSet :当queue是空的时候,消费者线程(t1、t2、t3)就不能消费任务,同理也应该阻塞,进入emptyWaitSet 。

消费者线程,也就是核心线程,后面我们统一叫核心线程

  • capcity:表示我们初始化创建任务队列的容量,与上面两个不一样,这个是放任务的,上面两个是放线程的。
  • BlockingQueue(int capcity):构造方法,用来初始化任务队列的容量
  • poll(long timeout, TimeUnit unit):核心线程带有超时的获取任务的方法,如果任务是空的就阻塞,而且是带有超时的阻塞,如果获取任务成功,说明queue获取后就不是满的状态了,所以应该唤醒fullWaitSet 中阻塞的生产者线程,让生产者线程继续生产任务。

注意:
为什么该方法把时间转化为纳秒?
主要是利用下面这个方法
nanos = emptyWaitSet.awaitNanos(nanos);
这个带有时间的阻塞方法的返回值是剩余的时间,可以理解为还应该再阻塞多少时间,主要是解决虚假唤醒的情况,如果阻塞的线程被其他无关线程唤醒了,唤醒的线程还会再过一编while循环,判断出当前队列queue还是空的,就会继续阻塞,但是阻塞的时间,就是剩余的时间了,而不再阻塞一次之前的老时间。否则整体上比我们传入的时间阻塞的长了。

  • offer(T task, long timeout, TimeUnit timeUnit) :生产者现场用于向队列queue中添加任务的方法,这个方法也是带有阻塞,如果queue是满的,就不应该添加任务,main线程就应该阻塞,我这里也是使用了超时阻塞,原因和上面一样,不想让它阻塞太久,任务添加不进就不添加,一直阻塞势必消耗CPU资源

  • size():获取当前任务的数量

  • tryPut(RejectPolicy rejectPolicy, T task):调用生产者提供的拒绝策略,为什么么说是尝试放进去,它的调用时时机是,核心线程用完了(t1、t2、t3都忙),如果任务队列满了,就执行拒绝策略,如果没满就放任务队列中

步骤3:自定义线程池

@Slf4j(topic = "c.ThreadPool")
class ThreadPool {
    // 任务队列
    private BlockingQueue<Runnable> taskQueue;
    // 线程集合
    private HashSet<Worker> workers = new HashSet<>();
    // 核心线程数
    private int coreSize;
    // 获取任务时的超时时间
    private long timeout;
    private TimeUnit timeUnit;
    private RejectPolicy<Runnable> rejectPolicy;
    // 执行任务
    public void execute(Runnable task) {
        // 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
        // 如果任务数超过 coreSize 时,加入任务队列暂存
        synchronized (workers) {
            if(workers.size() < coreSize) {
                Worker worker = new Worker(task);
                log.debug("新增 worker{}, {}", worker, task);
                workers.add(worker);
                worker.start();
            } else {
// taskQueue.put(task);
                // 1) 死等
                // 2) 带超时等待
                // 3) 让调用者放弃任务执行
                // 4) 让调用者抛出异常
                // 5) 让调用者自己执行任务
                taskQueue.tryPut(rejectPolicy, task);
            }
        }
    }
    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity,
                      RejectPolicy<Runnable> rejectPolicy) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapcity);
        this.rejectPolicy = rejectPolicy;
    }
    class Worker extends Thread{
        private Runnable task;
        public Worker(Runnable task) {
            this.task = task;
        }
        @Override
        public void run() {
            // 执行任务
            // 1) 当 task 不为空,执行任务
            // 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
// while(task != null || (task = taskQueue.take()) != null) {
            while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
                try {
                    log.debug("正在执行...{}", task);
                    task.run();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    task = null;
                }
            }
            synchronized (workers) {
                log.debug("worker 被移除{}", this);
                workers.remove(this);
            }
        }
    }
}

  • taskQueue:任务队列,这个队列中有我封装的取任务和添加任务的方法,以及线程的阻塞队列等属性

  • workers :存放工作线程,也就是核心线程

  • coreSize:定义核心线程的数量

  • timeout和timeUnit:任务时的超时时间,下面调用之前方法传参用

  • rejectPolicy:传参用

  • execute:线程池向外提供的执行方法

它的逻辑就是,如果线程池中初始化核心线程数没用完,就可以创作核心线程,用完就执行tryput(可能会执行拒绝策略。这也是为什么上面定义rejectPolicy)

  • ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity,
    RejectPolicy rejectPolicy):创建线程池的初始化方法,
    参数分别是:核心线程数、超时时间、时间单位、任务队列容量(queue),拒绝策略

  • class Worker extends Thread:线程实体,它的逻辑是先执行当前任务,如果当前任务执行完了,从任务队列中拿任务执行。

步骤4:测试

按照定义的决策策略分别演示:

1、 死等:初始化核心线程数是1,超时取任务的时间是1秒,任务队列容量是1,拒绝策略是死等
main线程提供了三个任务

public class TestPool2 {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(1,
                1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{
            // 1. 死等
            queue.put(task);
            task.run();
        });
        for (int i = 0; i < 3; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}", j);
            });
        }
    }
}
22:54:12.815 [main] DEBUG c.ThreadPool - 新增 workerThread[Thread-0,5,main], cn.itcast.text.TestPool2$$Lambda$2/1645995473@1376c05c
22:54:12.824 [main] DEBUG c.BlockingQueue - 加入任务队列 cn.itcast.text.TestPool2$$Lambda$2/1645995473@64a294a6
22:54:12.824 [main] DEBUG c.BlockingQueue - 等待加入任务队列 cn.itcast.text.TestPool2$$Lambda$2/1645995473@7e0b37bc ...
22:54:12.827 [Thread-0] DEBUG c.ThreadPool - 正在执行...cn.itcast.text.TestPool2$$Lambda$2/1645995473@1376c05c

现象:

  • 首先创建了一个核心线程,它拿着第一个任务开始执行
  • 由于任务执行周期特别长,核心线程一直处于忙碌状态,因此第二个任务,放在了任务队列等待核心线程空闲
  • 任务队列也满了同时核心线程也忙碌,第三个任务放不进去了,主线程main就进入阻塞(可以看看put方法)队列fullWaitSet一直死等,等待queue有位置

2、带超时等待:每个任务的执行周期是1秒,拒绝策略是main线程等待1.5秒,如果还添加不进去任务,就不添加了

@Slf4j(topic = "c.TestPool2")
public class TestPool2 {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(1,
                1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{
            // 2) 带超时等待
            queue.offer(task, 1500, TimeUnit.MILLISECONDS);
            task.run();
        });
        for (int i = 0; i < 3; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}", j);
            });
        }
    }
}
23:01:39.105 [main] DEBUG c.ThreadPool - 新增 workerThread[Thread-0,5,main], cn.itcast.text.TestPool2$$Lambda$2/1645995473@1376c05c
23:01:39.113 [main] DEBUG c.BlockingQueue - 加入任务队列 cn.itcast.text.TestPool2$$Lambda$2/1645995473@64a294a6
23:01:39.114 [main] DEBUG c.BlockingQueue - 等待加入任务队列 cn.itcast.text.TestPool2$$Lambda$2/1645995473@7e0b37bc ...
23:01:39.114 [Thread-0] DEBUG c.ThreadPool - 正在执行...cn.itcast.text.TestPool2$$Lambda$2/1645995473@1376c05c
23:01:40.121 [Thread-0] DEBUG c.TestPool2 - 0
23:01:40.121 [Thread-0] DEBUG c.ThreadPool - 正在执行...cn.itcast.text.TestPool2$$Lambda$2/1645995473@64a294a6
23:01:40.121 [main] DEBUG c.BlockingQueue - 加入任务队列 cn.itcast.text.TestPool2$$Lambda$2/1645995473@7e0b37bc
23:01:41.135 [Thread-0] DEBUG c.TestPool2 - 1
23:01:41.136 [Thread-0] DEBUG c.ThreadPool - 正在执行...cn.itcast.text.TestPool2$$Lambda$2/1645995473@7e0b37bc
23:01:42.151 [Thread-0] DEBUG c.TestPool2 - 2
23:01:43.151 [Thread-0] DEBUG c.ThreadPool - worker 被移除Thread[Thread-0,5,main]

可以看到三个任务都被执行了,如果main的拒绝策略是0.5秒呢?可以发现第三个任务没有执行

23:07:53.266 [main] DEBUG c.ThreadPool - 新增 workerThread[Thread-0,5,main], cn.itcast.text.TestPool2$$Lambda$2/1645995473@1376c05c
23:07:53.275 [main] DEBUG c.BlockingQueue - 加入任务队列 cn.itcast.text.TestPool2$$Lambda$2/1645995473@64a294a6
23:07:53.275 [main] DEBUG c.BlockingQueue - 等待加入任务队列 cn.itcast.text.TestPool2$$Lambda$2/1645995473@7e0b37bc ...
23:07:53.276 [Thread-0] DEBUG c.ThreadPool - 正在执行...cn.itcast.text.TestPool2$$Lambda$2/1645995473@1376c05c
23:07:54.282 [Thread-0] DEBUG c.TestPool2 - 0
23:07:54.283 [Thread-0] DEBUG c.ThreadPool - 正在执行...cn.itcast.text.TestPool2$$Lambda$2/1645995473@64a294a6
23:07:55.292 [Thread-0] DEBUG c.TestPool2 - 1
23:07:56.303 [Thread-0] DEBUG c.ThreadPool - worker 被移除Thread[Thread-0,5,main

3、 让调用者放弃任务执行

@Slf4j(topic = "c.TestPool2")
public class TestPool2 {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(1,
                1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{
  
            // 3) 让调用者放弃任务执行
             log.debug("放弃{}", task);

        });
        for (int i = 0; i < 3; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}", j);
            });
        }
    }
}

这个放弃比较简单,就直接让main线程打印一下任务,不执行任何添加操作

23:09:23.013 [main] DEBUG c.ThreadPool - 新增 workerThread[Thread-0,5,main], cn.itcast.text.TestPool2$$Lambda$2/1645995473@1376c05c
23:09:23.022 [main] DEBUG c.BlockingQueue - 加入任务队列 cn.itcast.text.TestPool2$$Lambda$2/1645995473@64a294a6
23:09:23.022 [main] DEBUG c.TestPool2 - 放弃cn.itcast.text.TestPool2$$Lambda$2/1645995473@7e0b37bc
23:09:23.023 [Thread-0] DEBUG c.ThreadPool - 正在执行...cn.itcast.text.TestPool2$$Lambda$2/1645995473@1376c05c
23:09:24.030 [Thread-0] DEBUG c.TestPool2 - 0
23:09:24.030 [Thread-0] DEBUG c.ThreadPool - 正在执行...cn.itcast.text.TestPool2$$Lambda$2/1645995473@64a294a6
23:09:25.045 [Thread-0] DEBUG c.TestPool2 - 1
23:09:26.061 [Thread-0] DEBUG c.ThreadPool - worker 被移除Thread[Thread-0,5,main]

4、 让调用者抛出异常

@Slf4j(topic = "c.TestPool2")
public class TestPool2 {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(1,
                1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{
   
           // 4) 让调用者抛出异常
          throw new RuntimeException("任务执行失败 " + task);

        });
        for (int i = 0; i < 4; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}", j);
            });
        }
    }
}

可以看出main线程看任务2放不进去了,就抛出异常,抛出异常后,任务3就不再尝试添加了

23:15:43.922 [main] DEBUG c.ThreadPool - 新增 workerThread[Thread-0,5,main], cn.itcast.text.TestPool2$$Lambda$2/1645995473@1376c05c
Exception in thread "main" java.lang.RuntimeException: 任务执行失败 cn.itcast.text.TestPool2$$Lambda$2/1645995473@7e0b37bc
23:15:43.930 [main] DEBUG c.BlockingQueue - 加入任务队列 cn.itcast.text.TestPool2$$Lambda$2/1645995473@64a294a6
	at cn.itcast.text.TestPool2.lambda$main$0(TestPool2.java:23)
	at cn.itcast.text.BlockingQueue.tryPut(TestPool2.java:231)
	at cn.itcast.text.ThreadPool.execute(TestPool2.java:73)
	at cn.itcast.text.TestPool2.main(TestPool2.java:29)
23:15:43.940 [Thread-0] DEBUG c.ThreadPool - 正在执行...cn.itcast.text.TestPool2$$Lambda$2/1645995473@1376c05c
23:15:44.954 [Thread-0] DEBUG c.TestPool2 - 0
23:15:44.955 [Thread-0] DEBUG c.ThreadPool - 正在执行...cn.itcast.text.TestPool2$$Lambda$2/1645995473@64a294a6
23:15:45.970 [Thread-0] DEBUG c.TestPool2 - 1
23:15:46.976 [Thread-0] DEBUG c.ThreadPool - worker 被移除Thread[Thread-0,5,main]

5、 让调用者自己执行任务
这里是让主线程

public class TestPool2 {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(1,
                1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{
            task.run();
        });
        for (int i = 0; i < 4; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}", j);
            });
        }
    }

因此任务2、3是mian自己执行的

23:14:21.457 [main] DEBUG c.ThreadPool - 新增 workerThread[Thread-0,5,main], cn.itcast.text.TestPool2$$Lambda$2/1645995473@1376c05c
23:14:21.465 [main] DEBUG c.BlockingQueue - 加入任务队列 cn.itcast.text.TestPool2$$Lambda$2/1645995473@64a294a6
23:14:21.465 [Thread-0] DEBUG c.ThreadPool - 正在执行...cn.itcast.text.TestPool2$$Lambda$2/1645995473@1376c05c
23:14:22.469 [Thread-0] DEBUG c.TestPool2 - 0
23:14:22.469 [main] DEBUG c.TestPool2 - 2
23:14:23.484 [main] DEBUG c.TestPool2 - 3
23:14:23.484 [Thread-0] DEBUG c.ThreadPool - 正在执行...cn.itcast.text.TestPool2$$Lambda$2/1645995473@64a294a6
23:14:24.488 [Thread-0] DEBUG c.TestPool2 - 1
23:14:25.491 [Thread-0] DEBUG c.ThreadPool - worker 被移除Thread[Thread-0,5,main]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值