使用 LinkedBlockingQueue 实现简易版线程池
一、线程池设计





二、为什么使用 LinkedBlockingQueue
1. BlockingQueue
2. ArrayBlockingQueue
3. DelayQueue
4. LinkedBlockingQueue
5. PriorityBlockingQueue
6. SynchronousQueue
package java.util.concurrent; /**
* 带有缓存的线程池
*/
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
7. 阻塞队列选择
- 队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE)。对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
- 数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
- 由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
- 实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReentrantLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
三、LinkedBlockingQueue 底层方法

- LinkedBlockingQueue继承于AbstractQueue,它本质上是一个FIFO(先进先出)的队列。
- LinkedBlockingQueue实现了BlockingQueue接口,它支持多线程并发。当多线程竞争同一个资源时,某线程获取到该资源之后,其它线程需要阻塞等待。
- LinkedBlockingQueue是通过单链表实现的。
- head是链表的表头。取出数据时,都是从表头head处获取。
- last是链表的表尾。新增数据时,都是从表尾last处插入。
- count是链表的实际大小,即当前链表中包含的节点个数。
- capacity是列表的容量,它是在创建链表时指定的。
- putLock是插入锁,takeLock是取出锁;notEmpty是“非空条件”,notFull是“未满条件”。通过它们对链表进行并发控制。
// 容量
private final int capacity; // 当前数量
private final AtomicInteger count = new AtomicInteger(0); // 链表的表头
transient Node<E> head; // 链表的表尾
private transient Node<E> last; // 用于控制删除元素的【取出锁】和锁对应的【非空条件】
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition(); // 用于控制添加元素的【插入锁】和锁对应的【非满条件】
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
- 对于插入操作,通过 putLock(插入锁)进行同步
- 对于取出操作,通过 takeLock(取出锁)进行同步
LinkedBlockingQueue 常用函数
// 创建一个容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue
LinkedBlockingQueue() // 创建一个容量是 Integer.MAX_VALUE 的 LinkedBlockingQueue,最初包含给定 collection 的元素,元素按该 collection 迭代器的遍历顺序添加
LinkedBlockingQueue(Collection<? extends E> c) // 创建一个具有给定(固定)容量的 LinkedBlockingQueue
LinkedBlockingQueue(int capacity) // 从队列彻底移除所有元素
void clear() // 将指定元素插入到此队列的尾部(如果立即可行且不会超出此队列的容量),在成功时返回 true,如果此队列已满,则返回 false
boolean offer(E e) // 将指定元素插入到此队列的尾部,如有必要,则等待指定的时间以使空间变得可用
boolean offer(E e, long timeout, TimeUnit unit) // 获取但不移除此队列的头;如果此队列为空,则返回 null
E peek() // 获取并移除此队列的头,如果此队列为空,则返回 null
E poll() // 获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)
E poll(long timeout, TimeUnit unit) // 将指定元素插入到此队列的尾部,如有队列满,则等待空间变得可用
void put(E e) // 返回理想情况下(没有内存和资源约束)此队列可接受并且不会被阻塞的附加元素数量
int remainingCapacity() // 从此队列移除指定元素的单个实例(如果存在)
boolean remove(Object o) // 返回队列中的元素个数
int size() // 获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)
E take()
/**
* 将指定元素插入到此队列的尾部(如果立即可行且不会超出此队列的容量)
* 在成功时返回 true,如果此队列已满,则返回 false
* 如果使用了有容量限制的队列,推荐使用add方法,add方法在失败的时候只是抛出异常
*/
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
// 如果队列已满,则返回false,表示插入失败
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
// 获取 putLock
putLock.lock();
try {
// 再次对【队列是不是满】的进行判断,如果不是满的,则插入节点
if (count.get() < capacity) {
enqueue(node); // 在队尾插入节点
c = count.getAndIncrement(); // 当前节点数量+1,并返回插入之前节点数量
if (c + 1 < capacity)
// 如果在插入元素之后,队列仍然未满,则唤醒notFull上的等待线程
notFull.signal();
}
} finally {
// 释放 putLock
putLock.unlock();
}
if (c == 0)
// 如果在插入节点前,队列为空,那么插入节点后,唤醒notEmpty上的等待线程
signalNotEmpty();
return c >= 0;
}
下面来看看 put(E e) 的源码:
/**
* 将指定元素插入到此队列的尾部,如有队列满,则等待空间变得可用
*
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException(); int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly(); // 可中断地获取 putLock
try {
// count 变量是被 putLock 和 takeLock 保护起来的,所以可以真实反映队列当前的容量情况
while (count.get() == capacity) {
notFull.await();
}
enqueue(node); // 在队尾插入节点
c = count.getAndIncrement(); // 当前节点数量+1,并返回插入之前节点数量
if (c + 1 < capacity)
// 如果在插入元素之后,队列仍然未满,则唤醒notFull上的等待线程
notFull.signal();
} finally {
putLock.unlock(); // 释放 putLock
}
if (c == 0)
// 如果在插入节点前,队列为空,那么插入节点后,唤醒notEmpty上的等待线程
signalNotEmpty();
}
/**
* 通知一个等待的take。该方法应该仅仅从put/offer调用,否则一般很难锁住takeLock
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock(); // 获取 takeLock
try {
notEmpty.signal(); // 唤醒notEmpty上的等待线程,意味着现在可以获取元素了
} finally {
takeLock.unlock(); // 释放 takeLock
}
}
/**
* 获取并移除此队列的头,如果此队列为空,则返回 null
*/
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock(); // 获取 takeLock
try {
if (count.get() > 0) {
x = dequeue(); // 获取队头元素,并移除
c = count.getAndDecrement(); // 当前节点数量-1,并返回移除之前节点数量
if (c > 1)
// 如果在移除元素之后,队列中仍然有元素,则唤醒notEmpty上的等待线程
notEmpty.signal();
}
} finally {
takeLock.unlock(); // 释放 takeLock
}
if (c == capacity)
// 如果在移除节点前,队列是满的,那么移除节点后,唤醒notFull上的等待线程
signalNotFull();
return x;
}
/**
* 取出并返回队列的头。若队列为空,则一直等待
*/
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
// 获取 takeLock,若当前线程是中断状态,则抛出InterruptedException异常
takeLock.lockInterruptibly();
try {
// 若队列为空,则一直等待
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue(); // 从队头取出元素
c = count.getAndDecrement(); // 取出元素之后,节点数量-1;并返回移除之前的节点数量
if (c > 1)
// 如果在移除元素之后,队列中仍然有元素,则唤醒notEmpty上的等待线程
notEmpty.signal();
} finally {
takeLock.unlock(); // 释放 takeLock
} if (c == capacity)
// 如果在取出元素之前,队列是满的,就在取出元素之后,唤醒notFull上的等待线程
signalNotFull();
return x;
}
/**
* 唤醒notFull上的等待线程,只能从 poll 或 take 调用
*/
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock(); // putLock 上锁
try {
notFull.signal(); // 唤醒notFull上的等待线程,意味着可以插入元素了
} finally {
putLock.unlock(); // putLock 解锁
}
}
四、简易版线程池代码实现
1. 注册成为 Spring Bean
package cn.com.gkmeteor.threadpool.utils; @Component
public class ThreadPoolUtil implements InitializingBean { public static int POOL_SIZE = 10; @Autowired
private ThreadExecutorService threadExecutorService; // 具体的线程处理类 private List<ThreadWithQueue> threadpool = new ArrayList<>(); /**
* 在所有基础属性初始化完成后,初始化当前类
*
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
for (int i = 0; i < POOL_SIZE; i++) {
ThreadWithQueue threadWithQueue = new ThreadWithQueue(i, threadExecutorService);
this.threadpool.add(threadWithQueue);
}
}
}
2. 轮询获取一个线程
public static int POOL_SIZE = 10; // 线程池容量
index = (++index) % POOL_SIZE; // index 是当前选中的线程下标
3. 参数入队和出队,线程运行和阻塞
package cn.com.gkmeteor.threadpool.utils; import cn.com.gkmeteor.threadpool.service.ThreadExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.util.concurrent.BlockingQueue; /**
* 带有【参数阻塞队列】的线程
*/
public class ThreadWithQueue extends Thread { public static int CAPACITY = 10; private Logger logger = LoggerFactory.getLogger(ThreadWithQueue.class); private BlockingQueue<String> queue; private ThreadExecutorService threadExecutorService; // 线程运行后的业务逻辑处理 private String threadName; public String getThreadName() {
return threadName;
} public void setThreadName(String threadName) {
this.threadName = threadName;
} /**
* 构造方法
*
* @param i 第几个线程
* @param threadExecutorService 线程运行后的业务逻辑处理
*/
public ThreadWithQueue(int i, ThreadExecutorService threadExecutorService) {
queue = new java.util.concurrent.LinkedBlockingQueue<>(CAPACITY);
threadName = "Thread(" + i + ")"; this.threadExecutorService = threadExecutorService; this.start();
} /**
* 将参数放到线程的参数队列中
*
* @param param 参数
* @return
*/
public String paramAdded(String param) {
String result = "";
if(queue.offer(param)) {
logger.info("参数已入队,{} 目前参数个数 {}", this.getThreadName(), queue.size());
result = "参数已加入线程池,等待处理";
} else {
logger.info("队列已达最大容量,请稍后重试");
result = "线程池已满,请稍后重试";
}
return result;
} public synchronized int getQueueSize() {
return queue.size();
} @Override
public void run() {
while (true) {
try {
String param = queue.take();
logger.info("{} 开始运行,参数队列中还有 {} 个在等待", this.getThreadName(), this.getQueueSize());
if (param.startsWith("contact")) {
threadExecutorService.doContact(param);
} else if (param.startsWith("user")) {
threadExecutorService.doUser(param);
} else {
logger.info("参数无效,不做处理");
}
logger.info("{} 本次处理完成", this.getThreadName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
了解了链接阻塞队列的底层方法后,使用起来就底气十足。具体来说:
五、总结
六、参考资料
- 阻塞队列,https://blog.csdn.net/f641385712/article/details/83691365
- 数组阻塞队列和链接阻塞队列,同事博客,https://blog.csdn.net/a314368439/article/details/82789367
- 链接阻塞队列,https://www.jianshu.com/p/9394b257fdde
- 延迟队列,https://blog.csdn.net/z69183787/article/details/80520851
- 优先级阻塞队列,https://blog.csdn.net/java_jsp_ssh/article/details/78515866
- 同步队列,https://segmentfault.com/a/1190000011207824
使用 LinkedBlockingQueue 实现简易版线程池的更多相关文章
- Java多线程之Executor框架和手写简易的线程池
目录 Java多线程之一线程及其基本使用 Java多线程之二(Synchronized) Java多线程之三volatile与等待通知机制示例 线程池 什么是线程池 线程池一种线程使用模式,线程池会维 ...
- python low版线程池
1.low版线程池设计思路:运用队列queue 将线程类名放入队列中,执行一个就拿一个出来import queueimport threading class ThreadPool(object): ...
- Java与Scala的两种简易版连接池
Java版简易版连接池: import java.sql.Connection; import java.sql.DriverManager; import java.util.LinkedList; ...
- 用java自制简易线程池(不依赖concurrent包)
很久之前人们为了继续享用并行化带来的好处而不想使用进程,于是创造出了比进程更轻量级的线程.以linux为例,创建一个进程需要申请新的自己的内存空间,从父进程拷贝一些数据,所以开销是比较大的,线程(或称 ...
- 简易线程池Thread Pool
1. 基本思路 写了个简易的线程池,基本的思路是: 有1个调度线程,负责维护WorkItem队列.管理线程(是否要增加工作线程).调度(把工作项赋给工作线程)等 线程数量随WorkItem的量动态调整 ...
- 基于Win32 SDK实现的一个简易线程池
利用C++实现了一个简易的线程池模型(基于Win32 SDK),方便使用多线程处理任务.共包含Thread.h.Thread.cpp.ThreadPool.h.ThreadPool.cpp四个源文件. ...
- Java线程池实现原理与技术(ThreadPoolExecutor、Executors)
本文将通过实现一个简易的线程池理解线程池的原理,以及介绍JDK中自带的线程池ThreadPoolExecutor和Executor框架. 1.无限制线程的缺陷 多线程的软件设计方法确实可以最大限度地发 ...
- Java中的线程池用过吧?来说说你是怎么理解线程池吧?
前言 Java中的线程池用过吧?来说说你是怎么使用线程池的?这句话在面试过程中遇到过好几次了.我甚至这次标题都想写成[Java八股文之线程池],但是有点太俗套了.虽然,线程池是一个已经被说烂的知识点了 ...
- Java并发编程:线程池的使用
Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...
随机推荐
- Spring Boot Admin-应用健康监控后台管理
Spring Boot Admin 用于监控基于 Spring Boot 的应用,它是在 Spring Boot Actuator 的基础上提供简洁的可视化 WEB UI. 1. 什么是Spring ...
- HTML自制计算器
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...
- 大数据vs计算机
大数据有两个方向,一个是偏计算机的,另一个是偏经济的.你学过Java,所以你可以偏将计算机 基础1. 读书<Introduction to Data Mining>,这本书很浅显易懂,没有 ...
- 循环语句作业与循环补充(continue与break)
作业: 1.使用while循环输入1 2 3 4 5 6 8 9 10(没有7) n = 1 while n < 11: if n == 7: pass else: print(n) n = n ...
- 第二阶段:2.商业需求文档MRD:3.MRD-目标用户分析
以上是对目标市场的分析! 用户描述是定性.市场统计是定量.用用户分类模型去剖析用户分类. 例子.做百度推广时候的用户分析.不同角色的关注点不同.三个情景:广告售卖,广告投放,分析评估. 用户的使用习惯 ...
- JDK1.8的HashMap实现原理和源码解析
哈希表(hash table)也叫散列表,是一种非常重要的数据结构.许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中的对应实现HashMap的 ...
- 一文详解滑动平均法、滑动平均模型法(Moving average,MA)
任何关于算法.编程.AI行业知识或博客内容的问题,可以随时扫码关注公众号「图灵的猫」,加入”学习小组“,沙雕博主在线答疑~此外,公众号内还有更多AI.算法.编程和大数据知识分享,以及免费的SSR节点和 ...
- .NET设计篇08-线程统一取消模型和跨线程访问UI
知识需要不断积累.总结和沉淀,思考和写作是成长的催化剂,输出倒逼输入 内容目录 一.线程统一取消模型1.取消令牌2.可以中断的线程1.设计一个中断函数2.创建CancellationTokenSour ...
- Java后台创建Socket服务接收硬件终端发送的数据
最近项目中有遇到后台接收硬件终端发送的数据并解析存储的需求,代码总结如下(有时间再来一一讲解,最近比较忙): @Override public void start() { ExecutorServi ...
- 1053 住房空置率 (20 分)C语言
在不打扰居民的前提下,统计住房空置率的一种方法是根据每户用电量的连续变化规律进行判断.判断方法如下: 在观察期内,若存在超过一半的日子用电量低于某给定的阈值 e,则该住房为"可能空置&quo ...