使用 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并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...
随机推荐
- tet-2
一.html和css部分 1.如何理解CSS的盒子模型? 标准盒子模型:宽度=内容的宽度(content)+ border + padding 低版本IE盒子模型:宽度=内容宽度(content+ ...
- Activiti工作流引擎学习(一)
1.部署对象和流程定义相关表:RepositoryService act_re_deployment: 部署对象表:一次部署的多个文件的信息,对于不需要的流程可以删除和修改 act_re_procde ...
- 牛客多校第一场 B Inergratiion
牛客多校第一场 B Inergratiion 传送门:https://ac.nowcoder.com/acm/contest/881/B 题意: 给你一个 [求值为多少 题解: 根据线代的知识 我们可 ...
- A non well formed numeric value encountered
在从数据库获取时间戳数据的时候返回的结果是varchar类型 然后在用date("Y-m-d H:i:s", 时间戳)的时候会报错A non well formed numeric ...
- Mybase desktop7.3破解
1.Mybase Desktop 7.3 安装包 百度云链接: 链接:https://pan.baidu.com/s/1mWZ2_Qmkf6aAX9CYgrN12A 提取码:vjw7 2.破解包 百度 ...
- java中List 和 Set 的区别
a. 特性 两个接口都是继承自Collection,是常用来存放数据项的集合,主要区别如下: ① List和Set之间很重要的一个区别是是否允许重复元素的存在,在List中允许插入重复的元 ...
- JIRA从8.1.0升级到8.3.0
1.程序目录 JIRA8.1.0 安装目录(以下简称原目录): /opt/atlassian/jira-8.1.0-bak JIRA8.1.0 HOME目录(以下简称原HOME): /var/atla ...
- 软RAID和硬RAID的区别
要实现RAID可以分为硬件实现和软件实现两种.所谓硬RAID就是指通过硬件实现,同理软件实现就作为软RAID. 硬RAID 就是用专门的RAID控制器将硬盘和电脑连接起来,RAID控制器负责将所有 ...
- Mysql 最全查询语句
基本查询语句及语法: select distinct from where group by having limit 一.单表查询 前期表与数据准备: # 创建一张部门表 create table ...
- $Poj3179\ Corral\ the\ Cows$ 二分+离散化+二维前缀和
Poj $Description$ 在一个二维平面上,有$N$颗草,每颗草的大小是$1*1$,左下角坐标为$x_i,y_i$.要求一个正方形,正方形的边平行于$x$或$y$轴,正方形里面包含至少$C$ ...