推荐阅读
JUC源码
并发工具
19 | CountDownLatch和CyclicBarrier让多线程步调一致
17 | ReadWriteLock:如何快速实现一个完备的缓存?
在上篇 Executor与线程池:如何创建正确的线程池?中,介绍了Java线程池是一种生产者-消费者模式,ThreadPoolExecutor的核心参数,以及使用线程池的注意事项。
本篇还是通过运用自己所掌握的知识、进行猜想、实现、再进而验证;
通过手写Java线程池,进一步理解生产者-消费者模式。
Java线程池,也是各大公司面试的重点,面试题也是层出不穷。这里先梳理几个简单的,来帮助我们理清Java线程池的脉络,猜测JDK是如何实现的。
问题1:为什么使用线程池?
节省对象创建销毁时间:线程的创建时间为T1,执行时间T2,销毁时间T3,减少T1和T3的时间
用线程池管理Thread,池化的对象能反复利用,提高程序效率
提高线程的可管理性
问题2:单核cpu机器上适不适合用多线程 ?
适合。如果是单线程,线程中需要等待IO时,此时CPU就空闲出来了。
问题3:线程什么时候会让出cpu ?
阻塞时Object.wait等待、sleep、yield、线程执行结束时。
问题4:让你实现一个线程池,你如何实现?
在手写线程池之前,我们应该考虑一下,该如何设计,有几个关键点,我们需要考虑:
(1)线程池中运行线程的个数,如何设定
(2)如何表示任务?线程池中如何保存未处理的任务
(3)如何向线程池提交任务
(4)如何终止线程池
下面来对这几个关键点,一一分析:
1.线程池中运行线程的个数,如何设定:这个线程数量,不能写死,应该由使用方设定,也就是实现时需要作为参数传入;
2.如何表示任务?线程池中如何保存未处理的任务:因为JDK中已有Runnable和Callable,均可表示任务,我们无需再重复定义。池中等待执行的任务,用一个线程安全的容器来保存。
3.如何向线程池提交任务:这是实现线程池的关键。
4.如何终止线程池:一种可行的方案是,将线程池中管理的所有线程均interrupt()中断。
生产者-消费者模式的线程池
线程池工作原理
➢接收任务,放入仓库(仓库使用阻塞队列)
➢工作线程从仓库取任务,执行
➢当没有任务时,工作线程阻塞,当有任务时唤醒线程执行
BlockingQueue,作为保存未处理任务的仓库,最好实现成有界的;意味着
如果仓库满了,再提交的任务就返回false。
工作线程,单独用Worker类来表示,一个Worker就相当于一个干活的工人。
线程池的终止
线程池有创建和启动,还要实现关闭功能:关闭后不再接受新任务,待现有任务都完成后,所有线程都结束(队列中的任务都要取完),也就是实现优雅关闭。
下面是简单实现的 固定线程数的线程池FixedThreadPool:
MyFixedThreadPool构造方法中创建了固定数量workSize的工作线程;
submit将提交的任务加入到阻塞队列;
Worker工作线程,不断的从队列取任务执行,如果线程池已被终止(working=false),仍然会将队列中存在的任务取完;
shutDown设置线程池终止,用线程中断的方式中断工作线程;
/**
* 固定数量的工作线程
*/
public class MyFixedThreadPool {
// 存放任务的阻塞队列
private BlockingQueue<Runnable> taskQueue;
// 保存所有Worker工作线程
private List<Worker> workers;
private volatile boolean working = true;// 是否关闭线程池
public MyFixedThreadPool(int taskSize, int workSize) {
if (taskSize < 0 || workSize < 0) {
throw new IllegalArgumentException("IllegalArgumentException");
}
taskQueue = new LinkedBlockingQueue<>(taskSize);
workers = Collections.synchronizedList(new ArrayList<Worker>(workSize));
// 初始化固定数量的工作线程
for (int i = 0; i < workSize; i++) {
Worker w = new Worker(this);
workers.add(w);
w.start();
}
}
//提交任务的方法
//如果仓库满了就返回false
public boolean submit(Runnable task) {
if (working) {
return taskQueue.offer(task);
}
return false;
}
public void shutDown() {
working = false;
for (Worker w : workers) {
// 这些状态的线程都是在shutDown()之前 就进行blockingQueue.take()阻塞take的线程
if (w.getState().equals(Thread.State.BLOCKED)
|| w.getState().equals(Thread.State.WAITING)
|| w.getState().equals(Thread.State.TIMED_WAITING)) {
w.interrupt();
}
}
}
class Worker extends Thread {
MyFixedThreadPool pool;
public Worker(MyFixedThreadPool pool) {
this.pool = pool;
}
@Override
public void run() {
int count = 0;
while (pool.working) {
Runnable task = null;
try {
if (pool.working) {
task = pool.taskQueue.take();//取不到就阻塞,底层会LockSuport.park当前线程,使其阻塞WAITING
} else {
task = pool.taskQueue.poll();// 取不到,返回特殊值null
}
} catch (InterruptedException e) {
e.printStackTrace();
}
if (task != null) {
task.run();
}
System.out.println(Thread.currentThread() + "线程运行了" + (++count));
}
}
}
public static void main(String[] args) throws InterruptedException {
MyFixedThreadPool pool = new MyFixedThreadPool(6, 3);
for (int i = 0; i < 20; i++) {
boolean success = pool.submit(() -> {
System.out.println("执行任务");
});
System.out.println("submit result=" + success);
}
// Thread.sleep(2000);
// System.out.println(pool.taskQueue.poll());//待上面执行完成,队列为空,poll()出的为null
// pool.shutDown();
}
}
源码在 https://github.com/ljheee/my-collection/blob/master/src/com/ljheee/concurrent/poll/MyFixedThreadPool.java
简单实现的 固定线程数的线程池FixedThreadPool有很多不足:
工作线程Worker数目,直接一次性创建,不是按需生成;
workers.add(worker);操作容器需要考虑并发安全,需要加锁
线程池状态管理简单,只有创建、和终止状态(working=false)
但我们的目的也是通过梳理线程池相关的问题,帮助理解线程池的工作的原理,以及找出自己实现的这个线程池存在哪些不足,后续能通过分析JDK源码中的线程池是如何设计来解决这些不足。
总结
通过今天实现的一个非常简单的线程池,基本可以通过它来理解线程池的工作原理。
线程池在 Java 并发编程领域非常重要,很多大厂的编码规范都要求必须通过线程池来管理线程。线程池和普通的池化资源有很大不同,线程池实际上是生产者-消费者模式的一种实现,理解生产者-消费者模式是理解线程池的关键所在。
再看JDK实现的线程池,最核心的是 ThreadPoolExecutor,通过名字也能看出来,它强调的是 Executor,而不是一般意义上的池化资源(Pool)。
后续会有 ThreadPoolExecutor 源码深入讲解,持续更新。
创建线程池设置合适的线程数非常重要,这部分内容,你可以参考 Java 线程(中):创建多少线程才是合适的?的内容。
(你是否也遇到过手写线程池的面试题呢?快来说说吧)
本文首发于 公众号 架构道与术(ToBeArchitecturer),欢迎关注、学习更多干货~
推荐阅读
JUC源码
并发工具
19 | CountDownLatch和CyclicBarrier让多线程步调一致
17 | ReadWriteLock:如何快速实现一个完备的缓存?