文章目录
通常,实现线程安全的情况有两种,一种是通过加锁,另一种通过CAS自旋。在前面的集合框架学习中,我们学习了几种通过
CAS自旋或重量级锁
synchronized实现的线程安全的集合,然后在某些环境下,CAS并不适合,下面我们了解下Java集合框架中通过可重入锁
ReentrantLock来实现的集合结构——阻塞队列。
锁:在同一时间只能被同一个线程持有
可重入锁ReentrantLock:可重入锁指线程在持有某重入锁时,可重复获取此锁。
1. 阻塞队列概述
1.1 线程阻塞的情况
- 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
- 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
1.2 阻塞队列的主要方法
| 方法类型 | 抛出异常 | 返回特殊值 | 使阻塞 | 阻塞超时限定 |
|---|---|---|---|---|
| 插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
| 移除 | remove() | poll() | take() | poll(time,unit) |
| 检查 | element() | peek() | — | — |
2. Java中的阻塞队列
下面7个方法,有些方法通过名称就可以看出底层实现(ArrayBlockQueue是ArrayQueue的阻塞版本),我们着重关注的点是阻塞(线程安全)的实现,以及不同阻塞队列间的差别。
2.1 ArrayBlockingQueue(公平锁/非公平锁)
用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,而为了保证公平性通常情况下会降低吞吐量。
我们可以使用以下代码创建一个公平的阻塞队列:
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable
{
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
//公平指线程以此排队获取锁,非公平指JVM按随机、就近原则分配锁(效率高)
lock = new ReentrantLock(fair); // 公平的重入锁
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
// 添加元素的方法
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock(); // 锁住,其他线程在执行到此处需等待
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
}
2.2 LinkedBlockingQueue(读写锁)
基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,此队列按照先进先出(FIFO)的原则对元素进行排序。而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
{
private final ReentrantLock takeLock = new ReentrantLock(); // 非公平的读锁
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock(); // 非公平的写锁
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
// ...
private final AtomicInteger count = new AtomicInteger();//原子整型,count++线程不安全
}
2.3 LinkedBlockingDeque
相比于上面两个阻塞队列,LinkedBlockingDeque的可重入锁只是非公平的,且读写共用一个锁。
2.4 PriorityBlockingQueue
PriorityQueue的阻塞版本,提供最基本的非公平单重入锁机制
2.5 DelayQueue(缓存失效、定时任务 )
通过操纵PriorityQueue来提供方法,非公平单重入锁实现阻塞。在此之外,其提供了一种延时
队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
{
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
// 当头为空或头部元素还不能获取时
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll();
} finally {
lock.unlock();
}
}
}
可用于:
- 缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了
- 定时任务调度:
2.6 SynchronousQueue(不存储数据、可用于传递数据)
是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。
SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另 外 一 个 线 程 使 用 , SynchronousQueue 的 吞 吐 量 高 于 LinkedBlockingQueue 和ArrayBlockingQueue。
2.7 LinkedTransferQueue
是 一 个 由 链 表 结 构 组 成 的 无 界 阻 塞 TransferQueue 队 列 。 相 对 于 其 他 阻 塞 队 列 ,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法
-
transfer 方法:如果当前有消费者正在等待接收元素(消费者使用 take()方法或带时间限制的poll()方法时),transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素被消费者消费了才返回。
-
tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否接收,方法立即返回。而 transfer 方法是必须等到消费者消费了才返回。
2118

被折叠的 条评论
为什么被折叠?



