目录
本篇学习笔记利用自定义线程池去更深入的理解JDK的线程池API。
线程是一种系统资源,每创建一个新的线程,就要占用一定的内存,当在高并发的情况下,来了很多任务,如果要为每个任务创建一个线程,将耗费很大的系统资源,对内存的占用很大。
线程并不是越多越好,频繁的上下文切换也降低系统性能,尤其在高并发的情况下。
出于以上的原因,有了线程池的出现。
线程池可以让线程重复利用,既可以内存的占用,也可以减少线程的数量,避免频繁发生上下文的切换。
自定义线程池
自定义线程池需要两个组件,ThreadPool(线程池),BlockingQueue(阻塞队列),这两个组件。
ThreadPool里会有一些线程,让这些线程能够重复的利用。
Blocking Queue,它体现的是在一种生产者和消费者模式下,平衡它们之间差异的组件。
ThreadPool里的线程 相当于消费者,它去不断的获取任务和执行任务。除此以外还有生产者线程,它会源源不断的产生新的任务,在这个模式下,它们的速率是不一致的。
比如生产者,如果迟迟没有提交新任务,线程池里的线程就需要在阻塞队列里等待去执行任务。
又或者,生产者产生了太多的任务,线程们忙不过来,多出来的任务也需要放进阻塞队列里面等待被执行。
所以阻塞队列再此扮演了很重要的角色,它是生产者和消费者之间的桥梁。
先从阻塞队列入手,来自定义线程池。
BlockingQueue 的编写思路
通过对BlockingQueue的结构理出如下思路,看看编写BlockingQueue需要些什么东西 :
BlockingQueue 是一个任务队列,用于存放任务对象。这个队列的数据结构应该选择一个链表,因为队列的特性是先进先出。
链表结构可以用 Deque 类,它是一个双向链表,通过ArrayDeque来实现链表,因为它的效率高于LinkedList。
还需要锁,线程池有多个线程,它们都需要从任务队列头部获取任务,多个线程不可能同时获取正任务,只有一个线程能获取成功,其他线程则需要等待拿去下个任务。所以需要利用锁来保护头部元素,同理队列尾部也应该受到锁的保护,因为可能有多个线程都往队列里添加新任务。
锁的实现选择 ReentrantLock,这样比较灵活。
还需要两个变量,一个消费者的条件变量,和一个生产者的条件变量,消费者在没有任务的时候需要等待,而生产者也不能无限制的往队列里添加元素,阻塞队列都有容量限制,在阻塞队列容量已满的时候,它也需要去等待。因此需要消费者和生产者的条件变量。
还需要为阻塞队列定义容量,因为阻塞队列要有容量限制。
阻塞队列还需要一些个方法,一个方法来获取元素,一个方法来添加元素,一个方法来获取阻塞队列的大小,因为需要知道队列里的任务还有多少个。
阻塞队列需要的属性和方法一览:
- 阻塞队列
- 锁
- 生产者和消费者的条件变量
- 阻塞获取的方法
- 阻塞添加的方法
- 得到队列任务多少的方法
结构一览:
类一览(仅做结构展示,方法还未实现):
/**
* @author Claw
* @date 2022/3/8 22:49.
*/
public class BlockingQueue<T> {
// 任务队列
private Deque<T> queue = new LinkedList();
// 锁
private ReentrantLock lock = new ReentrantLock();
// 生产者条件变量
private Condition fullWaitSet = lock.newCondition();
// 消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
// 容量
private int capacity;
/**
* 阻塞获取
*
* @return
*/
public T take() {
}
/**
* 阻塞添加
*
* @return
*/
public void pull() {
}
/**
* 获取任务数量
*
* @return
*/
public int size() {
}
}
阻塞队列方法编写
take方法要从队列中获取元素,需要判断队列是否为空,如果为空则需要等待, 如果不为空则拿取队列头部元素(队列先入先出特性),再唤醒在等待放入任务的生产者线程。
pull方法要为队列中添加元素,需要判断队列是否已满,如果已满则需要等待,如果队列没有满,则在尾部添加元素,同时唤醒等待拿去任务的消费者线程。
size方法则获取队列里的元素多少即可。
/**
* 阻塞获取
*
* @return
*/
public T take() {
lock.unlock();
try {
// 获取之前,判断队列是否为空,如果为空则需要等待
while (queue.isEmpty()) {
try {
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果不为空 则拿取队列头部元素 因为队列特性 拿取头部元素 也是移除头部元素 因此方法是removeFirst()
T t = queue.removeFirst();
// 唤醒等待放入队列的生产者线程
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
/**
* 阻塞添加
*
* @return
*/
public void pull(T element) {
lock.lock();
try {
// 当阻塞队列容量已满,则进入等待
while (queue.size() == capacity) {
try {
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 当阻塞队列没有满时,则可以往里添加元素(添加至尾部)
queue.addLast(element);
// 唤醒等待的消费者线程
emptyWaitSet.signal();
} finally {
lock.unlock();
}
}
/**
* 获取任务数量
*
* @return
*/
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}