LinkedBlockingQueue 学习

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ko0491/article/details/90903428

LinkedBlockingQueue

独占锁实现的阻塞队列 LinkedB!ockingQueue

LinkedB lockingQueue 也是使用单向链表实现的,其也有两个
Node ,分别用来存放首、尾节点, 并且还有一个初始值为 0 的原子变量 count ,用来记录
队列元素个数 。 另外还有两个 ReentrantLock 的实例,分别用来控制元素入队和出队的原
子性,其中 takeLock 用来控制同时只有一个线程可以从队列头获取元素,其他线程必须
等待 , putLock 控制同时只能有一个线程可以获取锁,在队列尾部添加元素,其他线程必
须等待。另外, notEmpty 和 notFull 是条件变量 ,它们内部都有一个条件队列用来存放进
队和出队时被阻塞的线程,其实这是生产者一消费者模型。

transient Node head;

/**

  • Tail of linked list.
  • Invariant: last.next == null
    */
    private transient Node last;

/** Lock held by take, poll, etc */
//执行take 、 poll等操作时需要获取该锁
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
//队列为空时,执行出队操作(比如take )的线程会被放入这个条件队列进行等待
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
//执行put 、 offer等操作时需妥获取该锁*
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
//当队列满时 , 执行进队操作( 比如put )的线程会被放入这个条件队71]进行等待
private final Condition notFull = putLock.newCondition();

/** Current number of elements */
//当前队列元素个数
private final AtomicInteger count = new AtomicInteger();

当调 用 线程在 LinkedBlockingQu eue 实例上执行 tak e 、 poll 等操作 时 需要获取到
tak eLock 锁,从而保证 同时 只有 一个线程可 以 操作链表头节点 。 另外由于条件变量
notEmpty 内部的条件队列的维护使用的是 takeLock 的锁状态管理机制,所以在调
用 notEmpty 的 await 和 signal 方法前调用线程必须先获取到 takeLock 锁,否则会抛
出 Illega!MonitorStateException 异常。 notEmpty 内 部则 维护着一个条件队列,当线
程获取到 takeLock 锁后调用 notEmpty 的 await 方法时,调用线程会被阻塞,然后
该线程会被放到 notEmpty 内部的条件队列进行等待,直到有线程调用了 notEmpty
的 signal 方法。

在 LinkedBlockingQu eu e 实例上执行 put 、 offer 等操作时需要获取到 putLock
锁,从而保证 同 时只有一 个 线程可以操作链表尾节点。同样由于条件变量
notFull 内 部 的 条件 队列 的 维护使用的是 putLock 的锁状态管理机制,所以在调用
notFull 的 await 和 si gn al 方法前调用线程必须先获取到 putLock 锁,否 则 会抛出
Illega!MonitorStateEx c eption 异常。 notFull 内部 则 维护着一 个 条件队列,当线程获
取到 putLock 锁后调用 notFull 的 await 方法时,调用线程会被阻塞 , 然后该线程会
被放到 notFull 内 部的条件队列进行等待,直到有线程调用了 notFull 的 signal 方法。

构造函数
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
//初始化首 、尾节 点 , 让它们指向哨兵节点
last = head = new Node(null);
}

//Node
static class Node {
E item;

/**
 * One of:
 * - the real successor Node
 * - this Node, meaning the successor is head.next
 * - null, meaning there is no successor (this is the last node)
 */
Node<E> next;

Node(E x) { item = x; }

}

offer-add添加 操作

public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException(“Queue full”);
}

public boolean offer(E e) {
//( 1 )为空元素则抛出空指针异常
if (e == null)
throw new NullPointerException();
//如采当前队列满则丢弃将要放入的元素, 然后返回 false
final AtomicInteger count = this.count;//当前容量
//是不是满了
if (count.get() == capacity)
return false;
//(3)构造新节点,获取putLock独占锁
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
//(4 )如采队列不满则进队列,并递增元素计数
if (count.get() < capacity) {
//添加新元素
enqueue(node);
//数量+1 递增计数器。
c = count.getAndIncrement();
// 5 判断如果新元素入队后队列还有空闲空间,则唤醒 notFull 的条件队列里面
//因为调用了 notFull 的 await 操作(比如执行 put 方法而队列满了的时候)而被阻塞的一个
//线程,因为队列现在有空闲所以这里可 以提前唤醒一个入队线程。
if (c + 1 < capacity)
notFull.signal();
}
} finally {
//释放锁 6
// 释放获取的 putLock 锁,这里要注意 ,锁的释放一定要在 finally 里面做,
//因为即使 t可块抛出异常了, finally 也是会被执行到 。另 外释放锁后其他因为调用 put 操
//作而被阻塞的线程将会有一个获取到该锁
putLock.unlock();
}
//7
if (c == 0)
//c==O 说明在执行代码( 6 )释放锁时队列里面至少有一个元素 ,队列
//里面有元素则执行 signalNotEmpty 操作 ,
signalNotEmpty();
// 8
return c >= 0;
}

/**

  • Links node at end of queue.
  • @param node the node
    */
    private void enqueue(Node node) {
    // assert putLock.isHeldByCurrentThread();
    // assert last.next == null;
    last = last.next = node;
    }

signalNotEmpty

private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}

该方法的作用就是激活 notEmpty 的条件队列中因为调用 notEmpty 的 await 方法(比
如调用 take 方法并且队列为空的时候)而被阻塞的一个线程,这也说明了调用条件变量的
方法前要获取对应的锁 。

offer 方法通过使用 putLock 锁保证了在队尾新增元素操作的原子’性。另外,
调用条件变量的方法前一定要记得获取对应的锁,并且注意进队时只操作队列链表的尾节点.

put 操作
public void put(E e) throws InterruptedException {
//( 1 )如果为空元素则抛出空指针异常
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node node = new Node(e);
//(2 )构建新节点,并获取独占 19\putLock
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
//(3 )如果队列满 ,则等待,进入阻塞
while (count.get() == capacity) {
notFull.await();
}
//添加在最后的元素
//( 4 )进队列并递增计数
enqueue(node);
//计数器自增1
c = count.getAndIncrement();
if (c + 1 < capacity)
//不满,可以继续添加,唤醒
notFull.signal();
} finally {
//解锁
putLock.unlock();
}
if (c == 0)
//唤醒
signalNotEmpty();
}

在代码( 2 )中使用 putLock.locklntenuptibly() 获取独占锁,相比在 offer 方法 中获取
独占锁的方法这个方法可以被中断 。具体地说就是当前线程在获取锁的过程中 , 如果被其
他线程设置了中断标志则 当前线程会抛 出 IntenuptedException 异常 ,所以 put 操作在获取
锁的过程中是可被中断的

代码 (3 ) 判断如 果当前队 列己 满, 则 调用 notFull 的 await() 方法 把 当 前 线程放入
notFull 的条件队列,当前线程被阻塞挂起后会释放获取 到 的 putLock 锁。由于 putLock 锁
被释放 了 ,所 以现在其他线程就有机会获取到 putLock 锁了

代码( 3 )在判断队列是否为空时为何使用 while 循环而不是 if 语句?这是考虑
到当前线程被虚假唤醒的问题,也就是其他线程没有调用 notFull 的 singal 方法时 notFull.
await()在某种情况下会自动返回 。 如果使用 if 语句那么虚假唤醒后会执行代码( 4 ) 的元
素入队操作,并且递增计数器 , 而这时候队列己经满了,从而导致队列元素个数大于队列
被设置的容量,进而导致程序出错。而使用 while 循环时,假如 notFull.await()被虚假唤醒 了,
那么再次循环检查当前队列是否己满,如果是则再次进行等待。

poll 操作
从队列头部获取并移除一个元素 , 如果队列为空则返回 null , 该方法是不阻塞的
public E poll() {
//获取当前容量大小
final AtomicInteger count = this.count;
//(l )队列为空则返回 null
if (count.get() == 0)
return null;
//(2 )获取独占锁
E x = null;
int c = -1;

final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
    //(3 )队列不空则出队并递减计数
    if (count.get() > 0) {
        //出队列
        x = dequeue();
        //减一,计数器
        c = count.getAndDecrement();
        if (c > 1)
        //4  唤醒 线程,告诉线程,当前队列不是空
            notEmpty.signal();
    }
} finally {
    //解锁 5
    takeLock.unlock();
}
//移除之后,有一个空从头再来
if (c == capacity)
//唤醒 6
    signalNotFull();
return x;

}

dequeue
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node h = head;
Node first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}

private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}

代码 (1 )判断如果当前队列为空,则直接返回 null 。
代码( 2 )获取独占锁 takeLock,当前线程获取该锁后,其他线程在调用 poll 或者
take 方法时会被阻塞挂起。

代码( 3 )判断如果当前队列不为空则进行出队操作,然后递减计数器。这里需要思
考,如何保证执行代码 3.1 时队列不空,而执行代码 3.2 时也一定不会空呢?毕竟这不是
原子性操作,会不会出现代码 3.1 判断队列不为空,但是执行代码 3.2 时队列为空了呢?
那么我们看在执行到代码 3.2 前在哪些地方会修改 count 的计数。由于当前线程己经拿到
了 takeLock 锁,所 以其他调用 poll 或者 take 方法的线程不可能会走到修改 count 计数的地
方 。 其实这时候如果能走到修改 count 计数的地方是因为其他线程调用了 put 和 offer 操作,
由于这两个操作不需要获取 takeLock 锁而获取的是 putLock 锁 , 但是在 put 和 offer 操作
内部是增加 count 计数值的,所以不会出现上面所说的情况 。其实只需要看在哪些地方递
减了 count 计数值即可 , 只有递减了 count 计数值才会出现上面说的 ,执行代码 3.1 时队列
不空 , 而执行代码 3 .2 时队列为空的情况。我们查看代码,只有在 poll 、 take 或者 remove
操作 的地方会递减 count 计数值,但是这三个方法都需要获取到 takeLock 锁才能进行操作,
而当前线程己经获取了 takeLock 锁,所以其他线程没有机会在当前’情况下递减 count 计数
值, 所 以看起来代码 3 . 1 、 3 . 2 不是原子性 的 ,但是它们是线程安全 的

代码( 4 )判断如果。 l 则说明当前线程移除掉队列里面的一个元素后队列不为空
C c 是删 除元素前队列元素个数),那么这时候就可以激活因为调用 take 方法而被阻塞到
notEmpty 的条件队列里面的一个线程

代码( 6 )说明当前线程移除队头元素前当前队列是满的 , 移除队头元素后当前队列
至少有一个空闲位置,那么这时候就可以调用 s igna!NotFull 激活因为调用 put 方法而被阻
塞到 notFull 的条件队列里的一个线程 ,

poll 代码逻辑 比较简单,值得注意的是,获取元素时只操作了队列的头节点 。

peek操作

获取队列头部元素但是不从队列里面移除它,如果 队列为空则返 回 null 。 该方法是不
阻塞的
public E peek() {
//1 获取容量大小
if (count.get() == 0)
return null;
//2 获取锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {

    Node<E> first = head.next;
    //3 
    if (first == null)
        return null;
    else
    //4 
        return first.item;
} finally {
    // 5
    takeLock.unlock();
}

}

代码 (3 ) 这里还 是 需要判断 firs t
是否为 null , 不 能直接执行代码(的 。 正常情况下执行到代码 。 〉 说明队列不为空 ,但
是代码 Cl )和 ( 2 ) 不是原子性操作 ,也就是在执行点( 1) 判断队列不空后, 在代码 ( 2 )
获取到锁前有可能其他线程执行 了 poll 或者 take 操作 导致 队列变为空。然后 当 前线程获
取锁后 , 直接执行代码( 4 ) ( first.item )会抛出 空指针异常。

take 操作-可中断
获取当前队列头部元素并从队列里面移除它 。 如果 队列为空则阻塞当前线程直到 队列
不为 空然后返回元素 ,如果在 阻塞时被其他线程设置 了中 断标志 , 则被阻塞线程会抛 出
I n terruptedExceptio n 异常而返回

public E take() throws InterruptedException {
E x;
int c = -1;
//获取当前容量
final AtomicInteger count = this.count;
//1 获取锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
//( 2 ) 当前队列为 空则阻塞拉起
while (count.get() == 0) {
notEmpty.await();
}
//出队列 (3 )出队并递减计数
x = dequeue();
//计算器 自减一
c = count.getAndDecrement();
//4
if (c > 1)
notEmpty.signal();
} finally {
//5 解锁
takeLock.unlock();
}
//6
if (c == capacity)
signalNotFull();
return x;
}

在代码( 1 )中,当前线程获取到独占锁,其他调用 take 或者 poll 操作的线程将会被
阻塞挂起。
代码( 2 )判断如果队列为空则阻塞挂起当前线程,并把当前线程放入 notEmpty 的条
件队列 。
代码( 3 )进行出队操作并递减计数。
代码( 4 )判断如果 c>l 则说明当前队列不为空,那么唤醒 notEmpty 的条件队列里面
的一个因为调用 take 操作而被阻塞的线程。
代码( 5 )释放锁 。
代码( 6 )判断如果 c == capacity 则说明 当前队列至少有一个空闲位置,那么激活条
件变量 notFull 的条件队列里面的一个因为调用 put 操作而被阻塞的线程 。

  1. reomove()操作 不可中断的
    public boolean remove(Object o) {
    //为空就返回false
    if (o == null) return false;
    //1.获取锁-双重加锁
    fullyLock();
    try {
    //2 遍历队列 找到则删除并返回 true
    for (Node trail = head, p = trail.next;
    p != null;
    trail = p, p = p.next) {
    //3
    if (o.equals(p.item)) {
    unlink(p, trail);
    return true;
    }
    }
    // 4 找不到则 iii回 fal se
    return false;
    } finally {
    //5 解锁
    fullyUnlock();
    }
    }

双重锁
void fullyLock() {
putLock.lock();
takeLock.lock();
}
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}

代码 (1 )通过如HyLock 获取双重锁,获取后,其他线程进行入队或者出队操作时’就
会被阻塞挂起 。

代码(2)遍历队列寻找要删除的元素,找不到则直接返回 false ,找到则执行 unlink 操作
void unlink(Node p, Node trail) {
// assert isFullyLocked();
// p.next is not changed, to allow iterators that are
// traversing p to maintain their weak-consistency guarantee.
p.item = null;
trail.next = p.next;
if (last == p)
last = trail;
//如果当前队列满 ,则删除后,也不忘记唤醒等待的线程
if (count.getAndDecrement() == capacity)
notFull.signal();
}

删除元素后 , 如果发现当前队列有空闲空间,则唤醒 notFull 的条件队列中的一个因为调用 put 方法而被阻塞的线程
代码( 5 )调用 fullyUnlock 方法使用与加锁顺序相反的顺序释放双重锁

由 于 remove 方法在删除指定元素前加 了两把锁,所 以在遍历队列查找指定元
素的过程中是线程安全的,并且此时其他调用入队、出队操作 的线程全部会被阻塞 。 另外,
获取多个资源锁的顺序与释放的顺序是相反的

size 操 作
public int size() {
return count.get();
}

由于进行出队、入队操作时的 count 是加了锁的 , 所以结果相比 ConcurrentLinkedQueue 的
size 方法比较准确 。

猜你喜欢

转载自blog.csdn.net/ko0491/article/details/90903428