数据结构之队列
队列(queue)是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
线程阻塞的队列
在多线程中,阻塞的意思是,在某些情况下会挂起线程,一旦条件成熟,被阻塞的线程就会被自动唤醒。也就是说,之前线程的wait和notify我们程序员需要自己控制,但有了这个阻塞队列之后我们程序员就不用担心了,阻塞队列会自动管理。
总结:我们把做put操作的线程当成生产者,把take操作的线程当成消费者。空队列时消费者阻塞,队列满时生产者阻塞。
队列是一种数据结构,数据结构我认为是三元的,包含它的元素、元素之间的关系、元素的操作算法。队列的底层还是数组实现,但是对元素的操作算法不同于数组,所以就变成一个新的数据结构。
分析他的核心方法(put、take),不同的put和take方法会又不同的返回响应,阻塞只是其中一种。下面方法规定了对数组操作的方法。
检查的意思是队列是否为空,队首元素是什么。
方法类型主要指具体的操作,我们按插入行来讲。
抛出异常:是指队列满时,不阻塞而是抛出异常处理
特殊值:是值添加元素时,返回值,提示是否插入成功。
阻塞:就是线程阻塞,直到添加进队列。
超时:阻塞的优化,设置一个超时时间,防止一值等待,不释放资源。
下面是ArrayBlockingQueue的源码,做参考
//抛出异常
public boolean add(E var1) {
if (this.offer(var1)) {
return true;
} else {
//这里抛出异常
throw new IllegalStateException("Queue full");
}
}
//返回boolean值
public boolean offer(E var1) {
checkNotNull(var1);
ReentrantLock var2 = this.lock;
var2.lock();
boolean var3;
try {
if (this.count == this.items.length) {
var3 = false;
return var3;
}
this.enqueue(var1);
var3 = true;
} finally {
var2.unlock();
}
return var3;
}
//阻塞
public void put(E var1) throws InterruptedException {
checkNotNull(var1);
ReentrantLock var2 = this.lock;
var2.lockInterruptibly();
try {
while(this.count == this.items.length) {
//这里阻塞
this.notFull.await();
}
this.enqueue(var1);
} finally {
var2.unlock();
}
}
//超时
public boolean offer(E var1, long var2, TimeUnit var4) throws InterruptedException {
checkNotNull(var1);
long var5 = var4.toNanos(var2);
ReentrantLock var7 = this.lock;
var7.lockInterruptibly();
try {
//返回值var8
boolean var8;
while(this.count == this.items.length) {
//控制超时
if (var5 <= 0L) {
var8 = false;
return var8;
}
var5 = this.notFull.awaitNanos(var5);
}
this.enqueue(var1);
var8 = true;
return var8;
} finally {
var7.unlock();
}
}
阻塞队列的实现
我们可以再JUC包中看到各种阻塞队列,下面是常用的三个队列以及它们之间的关系
Queue是与List平级的存在,它也有对应的实现。阻塞队列也只是队列的一种。
ArrayBlockingQueue:在创建时,必须指定队列长度,是一个有界队列。
LinkedBlockingQueue:创建不需要指定长度,默认为2147483647长度,为int的最大值。
SynchronousQueue:长度0的队列,put时要有take操作。
阻塞队列的线程池应用
什么池的意义:就是为了控制数量,复用元素。
线程池就是为了控制线程数量,复用线程。结合现在的多核CPU,减少线程的切换,可以提高CPU的利用率,提高性能。
JUC包的线程池结构,它的具体落地实现是ThreadPoolExecutor。我们使用Executors工具类创建的线程池,我们看底层其实都是ThreadPoolExecutor对象,只是参数不同而已。
public class Test {
public static void main(String[] args) {
//固定线程数
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
//一个线程数
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
//初始为0线程数,可创建扩容,最大2147483647
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
}
}
public static ExecutorService newFixedThreadPool(int var0) {
//固定线程数
return new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
}
public static ExecutorService newSingleThreadExecutor() {
//一个线程数
return new Executors.FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));
}
public static ExecutorService newCachedThreadPool() {
//初始为0线程数,可创建扩容,最大2147483647
return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
}
日常开发过程中上述的创建线程池的方式建议都不要使用,比如newSingleThreadExecutor()方法创建的线程池,它的阻塞队列长度为2147483647,这太大了。newSingleThreadExecutor()也是一样的。newCachedThreadPool()创建的阻塞队列是SynchronousQueue,可以用,但是它的线程创建最大数为2147483647,不可能创建这么多线程。
我应该自己创建ThreadPoolExecutor线程池对象,参数自己设置,接下来我们分析一下ThreadPoolExecutor有哪些参数可以配置。
下面是ThreadPoolExecutor的全参创建的构造方法,可以看出它一共可以设置七个参数。忘了也没事,看源码回忆。
public ThreadPoolExecutor(int var1, int var2, long var3, TimeUnit var5, BlockingQueue<Runnable> var6, ThreadFactory var7, RejectedExecutionHandler var8) {
this.ctl = new AtomicInteger(ctlOf(-536870912, 0));
this.mainLock = new ReentrantLock();
this.workers = new HashSet();
this.termination = this.mainLock.newCondition();
if (var1 >= 0 && var2 > 0 && var2 >= var1 && var3 >= 0L) {
if (var6 != null && var7 != null && var8 != null) {
this.acc = System.getSecurityManager() == null ? null : AccessController.getContext();
this.corePoolSize = var1;
this.maximumPoolSize = var2;
this.workQueue = var6;
this.keepAliveTime = var5.toNanos(var3);
this.threadFactory = var7;
this.handler = var8;
} else {
throw new NullPointerException();
}
} else {
throw new IllegalArgumentException();
}
}
- corePoolSize:线程核心数:线程最小生存数。
- maximumPoolSize:最大线程数,可以创建的最大线程数
- keepAliveTime:生存时间,线程在大于核心数时,线程的生存时间
- unit:时间单位
- workQueue:阻塞队列,可以缓存新的任务
- threadFactory:线程管理工厂,负责创建线程
- handler:拒绝策略,当线程数到达最大,且阻塞队列空间也已满,对新的任务的拒绝策略
(如果需要JDK1.8 中文API文档,可以私信我)
七大参数对应的结构图
新任务进来执行流程图
拒绝策略是当线程数最大,阻塞队列也满了,无法处理新任务时的动作。有哪些动作呢
- AbortPolicy:抛出异常,阻止系统正常运行
- CallerRunsPolicy:不接收也不抛异常,回退给生产者线程
- DiscardOldestPolicy:抛出队列中等待最长的任务,尝试将新任务添加到队列中
- DiscardPolicy:不接收不抛异常,直接丢弃
线程核心数于最大线程数怎么设置呢
分析我们当前的业务需求,是CPU密集型(算法复杂,会一直在执行计算),IO密集型(网络通信,会有大量阻塞)。
- CPU密集型:CPU核数+1线程数。可以减少线程切换(例子:8核则9线程)
- IO密集型:CPU核数/(1-阻塞系数)。阻塞系数一般在0.8-0.9之间。(例子:8核、阻塞系数0.9则80线程)