提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
线程池模型架构,如下图所示
图中描绘的是,三个消费线程或者说是核心线程 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]