夜光序言:
天空中最微弱的星 也有权利争取最美的灿烂。
正文:
以道御术 / 以术识道
✓ schedule (Runnable task, long delay, TimeUnit timeunit)
这一方法规划一个任务将被定期执行。该任务将会在首个 initialDelay 之后得到执行,然后每个 period 时间之后重复执行。
如果给定任务的执行抛出了异常,该任务将不再执行。
如果没有任何异常的话,这个任务将会持续循环执行到
ScheduledExecutorService 被关闭。
如果一个任务占用了比计划的时间间隔更长的时候,下一次执行将在当前执行结束执行才开始。计划任务在同一时间不会有多个线程同时执行。
✓ scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
这一方法规划一个任务将被定期执行。该任务将会在首个 initialDelay 之后得到执行,然后每个 period 时间之后重复执行。
如果给定任务的执行抛出了异常,该任务将不再执行。
如果没有任何异常的话,这个任务将会持续循环执行到 ScheduledExecutorService 被关闭。
如果一个任务占用了比计划的时间间隔更长的时候,下一次执行将在当前执行结束执行才开始。
计划任务在同一
时间不会有多个线程同时执行。
✓ scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)
除了 period 有不同的解释之外这个方法和 scheduleAtFixedRate() 非常像。
scheduleAtFixedRate() 方法中,period 被解释为前一个执行的开始和下一个执行的开始之间的间隔时间。而在本方法中,period 则被解释为前一个
执行的结束和下一个执行的结束之间的间隔。
因此这个延迟是执行结束之间的间隔,而不是执行开始之间的间隔。
ScheduledExecutorService 的关闭:
正如 ExecutorService,在你使用结束之后你需要把 ScheduledExecutorService 关闭掉。否则他将导致 JVM
继续运行,即使所有其他线程已经全被关闭。
你 可 以 使 用 从 ExecutorService 接 口 继 承 来 的 shutdown() 或 shutdownNow() 方 法 将
ScheduledExecutorService 关闭。参见 ExecutorService 关闭部分以获取更多信息。
➢ForkJoinPool 合并和分叉(线程池)
ForkJoinPool 在 Java 7 中被引入。它和
ExecutorService
很相似,除了一点不同。
ForkJoinPool 让我们可以很方便地把任务分裂成几个更小的任务,这些分裂出来的任务也将会提交给 ForkJoinPool。
任务可以继续分割成更小的子任务,只要它还能分割。
可能听起来有些抽象,因此本节中我们将会解释 ForkJoinPool 是如何工作的,还有任务分割是如何进行的。
合并和分叉的解释:
在我们开始看 ForkJoinPool 之前我们先来简要解释一下分叉和合并的原理。分叉和合并原理包含两个递归进行的步骤。
两个步骤分别是分叉步骤和合并步骤。
分叉:
一个使用了分叉和合并原理的任务可以将自己分叉(分割)为更小的子任务,
这些子任务可以被并发执行。如下图所示:
通过把自己分割成多个子任务,每个子任务可以由不同的 CPU 并行执行,或者被同一个 CPU 上的不同线程执行。
只有当给的任务过大,把它分割成几个子任务才有意义。
把任务分割成子任务有一定开销,因此对于小型任务,这个分割的消耗可能比每个子任务并发执行的消耗还要大。
什么时候把一个任务分割成子任务是有意义的,这个界限也称作一个阀值。这要看每个任务对有意义阀值的决定。
很大程度上取决于它要做的工作的种类。
合并:
当一个任务将自己分割成若干子任务之后,该任务将进入等待所有子任务的结束之中。
一旦子任务执行结束,该任务可以把所有结果合并到同一个结果。图示如下:
当然,并非所有类型的任务都会返回一个结果。
如果这个任务并不返回一个结果,它只需等待所有子任务执行完 毕。
也就不需要结果的合并啦。
所以我们可以将 ForkJoinPool 是一个特殊的线程池,它的设计是为了更好的配合 分叉-和-合并 任务分割的工作。
ForkJoinPool 也在 java.util.concurrent 包中,其完整类名为 java.util.concurrent.ForkJoinPool。
创建一个 ForkJoinPool:
你可以通过其构造子创建一个 ForkJoinPool。
作为传递给 ForkJoinPool 构造子的一个参数,你可以定义你期望的并行级别。并行级别表示你想要传递给 ForkJoinPool 的任务所需的线程或 CPU 数量。
以下是一个 ForkJoinPool 示例:
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
提交任务到 ForkJoinPool:
就像提交任务到 ExecutorService 那样,把任务提交到 ForkJoinPool。你可以提交两种类型的任务。
一种是没有任何返回值的(一个 “行动”),另一种是有返回值的(一个”任务”)。
这两种类型分别由 RecursiveAction 和RecursiveTask 表示。
接下来介绍如何使用这两种类型的任务,以及如何对它们进行提交。
RecursiveAction:
RecursiveAction 是一种没有任何返回值的任务。
它只是做一些工作,比如写数据到磁盘,然后就退出了。
一个
RecursiveAction 可以把自己的工作分割成更小的几块,这样它们可以由独立的线程或者 CPU 执行。
你可以通过继承来实现一个 RecursiveAction。示例如下
package com.hy.多线程高并发;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveAction;
public class MyRecursiveAction extends RecursiveAction {
private long workLoad = 0;
public MyRecursiveAction(long workLoad) {
this.workLoad = workLoad;
}
@Override
protected void compute() {
//if work is above threshold, break tasks up into smaller tasks
//翻译:如果工作超过门槛,把任务分解成更小的任务
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List<MyRecursiveAction> subtasks =
new ArrayList<MyRecursiveAction>();
subtasks.addAll(createSubtasks());
for(RecursiveAction subtask : subtasks){
subtask.fork();
}
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
}
}
private List<MyRecursiveAction> createSubtasks() {
List<MyRecursiveAction> subtasks =
new ArrayList<MyRecursiveAction>();
MyRecursiveAction subtask1 = new MyRecursiveAction(this.workLoad / 2);
MyRecursiveAction subtask2 = new MyRecursiveAction(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
public static void main(String[] args) {
}
}
例子很简单。MyRecursiveAction 将一个虚构的 workLoad 作为参数传给自己的构造子。
如果 workLoad 高于
一个特定阀值,该工作将被分割为几个子工作,子工作继续分割。
如果 workLoad 低于特定阀值,该工作将由
MyRecursiveAction 自己执行。
你可以这样规划一个 MyRecursiveAction 的执行:
package com.hy.多线程高并发;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
public class MyRecursiveAction extends RecursiveAction {
private long workLoad = 0;
public MyRecursiveAction(long workLoad) {
this.workLoad = workLoad;
}
@Override
protected void compute() {
//if work is above threshold, break tasks up into smaller tasks
//翻译:如果工作超过门槛,把任务分解成更小的任务
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List<MyRecursiveAction> subtasks =
new ArrayList<MyRecursiveAction>();
subtasks.addAll(createSubtasks());
for(RecursiveAction subtask : subtasks){
subtask.fork();
}
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
}
}
private List<MyRecursiveAction> createSubtasks() {
List<MyRecursiveAction> subtasks =
new ArrayList<MyRecursiveAction>();
MyRecursiveAction subtask1 = new MyRecursiveAction(this.workLoad / 2);
MyRecursiveAction subtask2 = new MyRecursiveAction(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
public static void main(String[] args) {
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
//创建一个没有返回值的任务
MyRecursiveAction myRecursiveAction = new MyRecursiveAction(24);
//ForkJoinPool 执行任务
forkJoinPool.invoke(myRecursiveAction);
}
}
RecursiveTask:
RecursiveTask 是一种会返回结果的任务。
它可以将自己的工作分割为若干更小任务,并将这些子任务的执行结
果合并到一个集体结果。可以有几个水平的分割和合并。以下是一个 RecursiveTask 示例:
package com.hy.多线程高并发;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveTask;
public class MyRecursiveTask extends RecursiveTask<Long> {
private long workLoad = 0;
public MyRecursiveTask(long workLoad) {
this.workLoad = workLoad;
}
protected Long compute() {
//if work is above threshold, break tasks up into smaller tasks
//hy:you should try you best to
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List<MyRecursiveTask> subtasks =
new ArrayList<MyRecursiveTask>();
subtasks.addAll(createSubtasks());
for(MyRecursiveTask subtask : subtasks){
subtask.fork();
}
long result = 0;
for(MyRecursiveTask subtask : subtasks) {
result += subtask.join();
}
return result;
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
return workLoad * 3;
}
}
private List<MyRecursiveTask> createSubtasks() {
List<MyRecursiveTask> subtasks =
new ArrayList<MyRecursiveTask>();
MyRecursiveTask subtask1 = new MyRecursiveTask(this.workLoad / 2);
MyRecursiveTask subtask2 = new MyRecursiveTask(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
public static void main(String[] args) {
//测试运行一下
}
}
除 了 有 一 个 结 果 返 回 之 外 , 这 个 示 例 和 RecursiveAction 的 例 子 很 像 。
MyRecursiveTask 类 继 承 自RecursiveTask<Long>,这也就意味着它将返回一个 Long 类型的结果。
MyRecursiveTask 示例也会将工作分割为子任务,并通过 fork() 方法对这些子任务计划执行。
此外,本示例还通过调用每个子任务的 join() 方法收集它们返回的结果。子任务的结果随后被合并到一个更大的结果,并最终将其返
回。
对于不同级别的递归,
这种子任务的结果合并可能会发生递归
。
你可以这样规划一个 RecursiveTask:
package com.hy.多线程高并发;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class MyRecursiveTask extends RecursiveTask<Long> {
private long workLoad = 0;
public MyRecursiveTask(long workLoad) {
this.workLoad = workLoad;
}
protected Long compute() {
//if work is above threshold, break tasks up into smaller tasks
//hy:you should try you best to
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List<MyRecursiveTask> subtasks =
new ArrayList<MyRecursiveTask>();
subtasks.addAll(createSubtasks());
for(MyRecursiveTask subtask : subtasks){
subtask.fork();
}
long result = 0;
for(MyRecursiveTask subtask : subtasks) {
result += subtask.join();
}
return result;
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
return workLoad * 3;
}
}
private List<MyRecursiveTask> createSubtasks() {
List<MyRecursiveTask> subtasks =
new ArrayList<MyRecursiveTask>();
MyRecursiveTask subtask1 = new MyRecursiveTask(this.workLoad / 2);
MyRecursiveTask subtask2 = new MyRecursiveTask(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
public static void main(String[] args) {
//测试运行一下
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
//创建一个有返回值的任务
MyRecursiveTask myRecursiveTask = new MyRecursiveTask(128);
//线程池执行并返回结果
long mergedResult = forkJoinPool.invoke(myRecursiveTask);
System.out.println("mergedResult = " + mergedResult);
}
}
注意: ForkJoinPool.invoke() 方法的调用来获取最终执行结果的。
B. 并发队列-阻塞队列
常用的并发队列有阻塞队列和非阻塞队列,
前者使用锁实现,后者则使用 CAS 非阻塞算法实现
。
PS:至于非阻塞队列是靠 CAS 非阻塞算法,在这里不再介绍,大家只用知道,Java 非阻塞队列是使用 CAS 算法来实现的就可
以。感兴趣的童鞋可以维基网上自行学习.
下面我们先介绍阻塞队列。
阻塞队列:
阻塞队列 (BlockingQueue)是 Java util.concurrent 包下重要的数据结构,BlockingQueue 提供了线程安全的队列访问方式:
当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;
从阻塞队列取数据时,如 果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于 BlockingQueue 实现的。
➢BlockingQueue 阻塞队列
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。
下图是对这个原理的阐述:
一个线程往里边放,另外一个线程从里边取的一个 BlockingQueue。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。
也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。
它会一直处于阻塞之中, 直到负责消费的线程从队列中拿走一个对象。
负责消费的线程将会一直从该阻塞队列中拿出对象。
如果消费线程尝试
去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
BlockingQueue 的方法:
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。
这些方法如下:
阻塞队列提供了四种处理方法:
四组不同的行为方式解释:
抛异常:
如果试图的操作无法立即执行,抛一个异常。
特定值:
如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
阻塞:
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
超时:
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定
值。返回一个特定值以告知该操作是否成功(典型的是 true / false)。
无法向一个 BlockingQueue 中插入 null。如果你试图插入 null,BlockingQueue 将会抛出一个
NullPointerException.
BlockingQueue 的实现类:
BlockingQueue 是个接口,你需要使用它的实现之一来使用 BlockingQueue,Java.util.concurrent 包下具有以下 BlockingQueue 接口的实现类:
ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个
数组里。
有界也就意味着,它不能够存储无限多数量的元素。
它有一个同一时间能够存储元素数量的上限。
你
可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实
现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。
注入其中的元素必须实 java.util.concurrent.Delayed 接口。
LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。
如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
PriorityBlockingQueue : PriorityBlockingQueue 是 一 个 无 界 的 并 发 队 列 。 它 使 用 了 和 类
java.util.PriorityQueue 一 样 的 排 序 规 则 。 你 无 法 向 这 个 队 列 中 插 入 null 值 。
所 有 插 入 到
PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于
你自己的 Comparable 实现。
SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。
如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队
列中抽走。
同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点
➢ArrayBlockingQueue 阻塞队列
ArrayBlockingQueue 类图
如上图 ArrayBlockingQueue 内部有个数组 items 用来存放队列元素,putindex 下标标示入队元素下标,
takeIndex 是出队下标,count 统计队列元素个数,从定义可知道并没有使用 volatile 修饰,这是因为访问这些变量使用都是在锁块内,并不存在可见性问题。
另外有个独占锁 lock 用来对出入队操作加锁,这导致同时只有一个线程可以访问入队出队,另外 notEmpty,notFull 条件变量用来进行出入队的同步。
另外构造函数必须传入队列大小参数,所以为有界队列,默认是 Lock 为非公平锁。
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
ps:
所谓公平锁:
就是在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到自己。
非公平锁:
比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式
ArrayBlockingQueue 方法
✓ offer 方法
在队尾插入元素,如果队列满则返回 false,否者入队返回 true。
public boolean offer(E e) {
//e 为 null,则抛出 NullPointerException 异常
checkNotNull(e);
//获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//如果队列满则返回 false
if (count == items.length)
return false;
else {
//否者插入元素
insert(e);
return true;
}
} finally {
//释放锁
lock.unlock();
} }
private void insert(E x) {
//元素入队
items[putIndex] = x;
//计算下一个元素应该存放的下标
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
//循环队列,计算下标
final int inc(int i) {
return (++i == items.length) ? 0 : i;
}
这里由于在操作共享变量前加了锁,所以不存在内存不可见问题,加过锁后获取的共享变量都是从主内存获取的,
而不是在 CPU 缓存或者寄存器里面的值,释放锁后修改的共享变量值会刷新会主内存中。
另外这个队列是使用循环数组实现,所以计算下一个元素存放下标时候有些特殊。
另外 insert 后调用notEmpty.signal();
是为了激活调用 notEmpty.await()阻塞后放入 notEmpty 条件队列中的线程。
✓ Put 操作
在队列尾部添加元素,如果队列满则等待队列有空位置插入后返回。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//获取可被中断锁
lock.lockInterruptibly();
try {
//如果队列满,则把当前线程放入 notFull 管理的条件队列
while (count == items.length)
notFull.await();
//插入元素
insert(e);
} finally {
lock.unlock();
}
}
需要注意的是如果队列满了那么当前线程会阻塞,知道出队操作调用了 notFull.signal 方法激活该线程。
代码逻辑很简单,但是这里需要思考一个问题为啥调用 lockInterruptibly 方法而不是 Lock 方法。
我的理解是因为调用了条件变量的 await()方法,而 await()方法会在中断标志设置后抛出 InterruptedException 异常后退出,所以还不如在加锁时候先看中断标志是不是被设置了,如果设置了直接抛出 InterruptedException 异常,就不用再去获取锁了。
然后 看了其他并发类里面凡是调用了 await 的方法获取锁时候都是使用的 lockInterruptibly 方法而不是 Lock
也验证了这个想法
✓ Poll 操作
从队头获取并移除元素,队列为空,则返回 null
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//当前队列为空则返回 null,否者
return (count == 0) ? null : extract();
} finally {
lock.unlock();
}
}
private E extract() {
final Object[] items = this.items;
//获取元素值
E x = this.<E>cast(items[takeIndex]);
//数组中值值为 null;
items[takeIndex] = null;
//队头指针计算,队列元素个数减一
takeIndex = inc(takeIndex);
--count;
//发送信号激活 notFull 条件队列里面的线程
notFull.signal();
return x;
}
✓ Take 操作
从队头获取元素,如果队列为空则阻塞直到队列有元素。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//队列为空,则等待,直到队列有元素
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}
需要注意的是如果队列为空
当前线程会被挂起放到 notEmpty 的条件队列里面,直到入队操作执行调用 notEmpty.signal 后当前线程才会被激活,await 才会返回
✓ Peek 操作
返回队列头元素但不移除该元素,队列为空,返回 null。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//队列为空返回 null,否者返回头元素
return (count == 0) ? null : itemAt(takeIndex);
} finally {
lock.unlock();
}
}
final E itemAt(int i) {
return this.<E>cast(items[i]);
}
✓ Size 操作
获取队列元素个数,非常精确因为计算 size 时候加了独占锁,其他线程不能入队或者出队或者删除元素。
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
✓ ArrayBlockingQueue 小结
ArrayBlockingQueue 通过使用全局独占锁实现同时只能有一个线程进行入队或者出队操作,这个锁的粒度比较大,有点类似在方法上添加 synchronized 的意味。
其中 offer,poll 操作通过简单的加锁进行入队出队操作,而 put,take 则使用了条件变量实现如果队列满则等待,如果队列空则等待,然后分别在出队和入队操作中发送信号激活等待线程实现同步。
另外相比 LinkedBlockingQueue,ArrayBlockingQueue 的 size 操作的结果是精确的,因为计算前加了
全局锁。
ArrayBlockingQueue 示例
需求:在多线程操作下,一个数组中最多只能存入 3 个元素。多放入不可以存入数组,或等待某线程对数组中某个元素取走才能放入,要求使用 java 的多线程来实现。
package com.hy.多线程高并发;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* @Description:
* ArrayBlockingQueue 示例
* 需求:在多线程操作下,一个数组中最多只能存入 3 个元素。
* 多放入不可以存入数组,或等待某线程对数组中某个元素取走才能放入
* 要求使用 java 的多线程来实现。
* @Param:
* @return:
* @Author: Hy
* @Date: 2019
*/
public class BlockingQueueTest {
public static void main(String[] args) {
final BlockingQueue queue = new ArrayBlockingQueue(3);
for(int i=0;i<2;i++){
new Thread(){
public void run(){
while(true){
try {
Thread.sleep((long)(Math.random()*1000));
System.out.println(Thread.currentThread().getName() + "准备放数据!");
queue.put(1);
System.out.println(Thread.currentThread().getName() + "已经放了数据," +
"队列目前有" + queue.size() + "个数据");
} catch (InterruptedException e) {
e.printStackTrace();
} } }
}.start();
}
new Thread(){
public void run(){
while(true){
try {
//将此处的睡眠时间分别改为 100 和 1000,观察运行结果
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "准备取数据!");
System.err.println(queue.take());
System.out.println(Thread.currentThread().getName() + "已经取走数据," +
"队列目前有" + queue.size() + "个数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
➢LinkedBlockingQueue 阻塞队列
LinkedBlockingQueue 类图
LinkedBlockingQueue 中也有两个 Node 分别用来存放首尾节点,并且里面有个初始值为 0 的原子变量 count用来记录队列元素个数,另外里面有两个 ReentrantLock 的独占锁,分别用来控制元素入队和出队加锁,其中 takeLock
用来控制同时只有一个线程可以从队列获取元素,其他线程必须等待
putLock 控制同时只能有一个线程可以获取锁去添加元素,其他线程必须等待。另外 notEmpty 和 notFull 用来实现入队和出队的同步。
另外由于出入队是两个非公平独占锁,所以可以同时又一个线程入队和一个线程出队,其实这个是个生产者-消费者模型,如下类图:
/** 通过 take 取出进行加锁、取出 */
private final ReentrantLock takeLock = new ReentrantLock();
/** 等待中的队列等待取出 */
private final Condition notEmpty = takeLock.newCondition();
/*通过 put 放置进行加锁、放置*/
private final ReentrantLock putLock = new ReentrantLock();
/** 等待中的队列等待放置 */
private final Condition notFull = putLock.newCondition();
/* 记录集合中的个数(计数器) */
private final AtomicInteger count = new AtomicInteger(0);
LinkedBlockingQueue 方法
ps:下面介绍 LinkedBlockingQueue 用到很多 Lock 对象。详细可以查找 Lock 对象的介绍
✓ 带时间的 Offer 操作-生产者
在 ArrayBlockingQueue 中已经简单介绍了 Offer()方法,LinkedBlocking 的 Offer 方法类似,在此就不过多去介绍。
这次我们从介绍下带时间的 Offer 方法
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
//空元素抛空指针异常
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//获取可被中断锁,只有一个线程克获取
putLock.lockInterruptibly();
try {
//如果队列满则进入循环
while (count.get() == capacity) {
//nanos<=0 直接返回
if (nanos <= 0)
return false;
//否者调用 await 进行等待,超时则返回<=0(1)
nanos = notFull.awaitNanos(nanos);
}
//await 在超时时间内返回则添加元素(2)
enqueue(new Node<E>(e));
c = count.getAndIncrement();
//队列不满则激活其他等待入队线程(3)
if (c + 1 < capacity)
notFull.signal();
} finally {
//释放锁
putLock.unlock();
}
//c==0 说明队列里面有一个元素,这时候唤醒出队线程(4)
if (c == 0)
signalNotEmpty();
return true;
}
private void enqueue(Node<E> node) {
last = last.next = node;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
✓ 带时间的 poll 操作-消费者
获取并移除队首元素,在指定的时间内去轮询队列看有没有首元素有则返回,否者超时后返回 null。
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//出队线程获取独占锁
takeLock.lockInterruptibly();
try {
//循环直到队列不为空
while (count.get() == 0) {
//超时直接返回 null
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
//出队,计数器减一
x = dequeue();
c = count.getAndDecrement();
//如果出队前队列不为空则发送信号,激活其他阻塞的出队线程
if (c > 1)
notEmpty.signal();
} finally {
//释放锁
takeLock.unlock();
}
//当前队列容量为最大值-1 则激活入队线程。
if (c == capacity)
signalNotFull();
return x;
}
首先获取独占锁,然后进入循环当当前队列有元素才会退出循环,或者超时了,直接返回 null。
超时前退出循环后,就从队列移除元素,然后计数器减去一,如果减去 1 前队列元素大于 1 则说明当前移除后队列还有元素,那么就发信号激活其他可能阻塞到当前条件信号的线程。
最后如果减去 1 前队列元素个数=最大值,那么移除一个后会腾出一个空间来,这时候可以激活可能存在的入队阻塞线程。