LinkedBlockingDeque详解,源码,原理和常用方法,使用场景介绍

LinkedBlockingDeque详解

LinkedBlockingDeque介绍

【1】LinkedBlockingDeque是一个基于链表实现的双向阻塞队列,默认情况下,该阻塞队列的大小为Integer.MAX_VALUE,可以看做无界队列,但也可以设置容量限制,作为有界队列。

【2】相比于其他阻塞队列,LinkedBlockingDeque 多了 addFirst、addLast、peekFirst、peekLast 等方法。以first结尾的方法,表示插入、获取或移除双端队列的第一个元素。以 last 结尾的方法,表示插入、获取或移除双端队列的最后一个元素。但本质上并没有优化锁的竞争情况,因为不管是从队首还是队尾,都是在竞争同一把锁,只不过数据插入和获取的方式多了。

LinkedBlockingDeque的源码分析

【1】属性值

//典型的双端链表结构
static final class Node<E> {
    
    
    E item; //存储元素
    Node<E> prev;   //前驱节点
    Node<E> next; //后继节点

    Node(E x) {
    
    
        item = x;
    }
}
// 链表头  本身是不存储任何元素的,初始化时item指向null
transient Node<E> first;
// 链表尾
transient Node<E> last;
// 元素数量
private transient int count;
// 容量,指定容量就是有界队列
private final int capacity;
//重入锁
final ReentrantLock lock = new ReentrantLock();
// 当队列无元素时,锁会阻塞在notEmpty条件上,等待其它线程唤醒
private final Condition notEmpty = lock.newCondition();
// 当队列满了时,锁会阻塞在notFull上,等待其它线程唤醒
private final Condition notFull = lock.newCondition();

【2】构造函数

public LinkedBlockingDeque() {
    
    
    // 如果没传容量,就使用最大int值初始化其容量
    this(Integer.MAX_VALUE);
}

public LinkedBlockingDeque(int capacity) {
    
    
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
}

public LinkedBlockingDeque(Collection<? extends E> c) {
    
    
    this(Integer.MAX_VALUE);
    final ReentrantLock lock = this.lock;
    lock.lock(); // 为保证可见性而加的锁
    try {
    
    
        for (E e : c) {
    
    
            if (e == null)
                throw new NullPointerException();
            //从尾部插入元素,插入失败抛出异常
            if (!linkLast(new Node<E>(e)))
                throw new IllegalStateException("Deque full");
        }
    } finally {
    
    
        lock.unlock();
    }
}

【3】核心方法分析

1)入队方法

//添加头结点元素
public void addFirst(E e) {
    
    
    //如果添加失败,抛出异常
    if (!offerFirst(e))
        throw new IllegalStateException("Deque full");
}

//添加尾结点元素
public void addLast(E e) {
    
    
    //如果添加失败,抛出异常
    if (!offerLast(e))
        throw new IllegalStateException("Deque full");
}

//添加头结点元素
public boolean offerFirst(E e) {
    
    
    //添加的元素为空 抛出空指针异常
    if (e == null) throw new NullPointerException();
    //将元素构造为结点
    Node<E> node = new Node<E>(e);
    //这边会加锁,并调用添加头结点插入的核心方法
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        return linkFirst(node);
    } finally {
    
    
        //解锁
        lock.unlock();
    }
}

//添加尾结点元素
public boolean offerLast(E e) {
    
    
    //添加的元素为空 抛出空指针异常
    if (e == null) throw new NullPointerException();
    //将元素构造为结点
    Node<E> node = new Node<E>(e);
    //这边会加锁,并调用添加尾结点插入的核心方法
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        return linkLast(node);
    } finally {
    
    
        lock.unlock();
    }
}

//头部插入
private boolean linkFirst(Node<E> node) {
    
    
    //当前容量大于队列最大容量时,直接返回插入失败
    if (count >= capacity)
        return false;
    //队列中的头结点
    Node<E> f = first;
    //原来的头结点作为 新插入结点的后一个结点
    node.next = f;
    //替换头结点 为新插入的结点
    first = node;
    //尾结点不存在,将尾结点置为当前新插入的结点
    if (last == null)
        last = node;
    else
        //原来的头结点的上一个结点为当前新插入的结点
        f.prev = node;
    //当前容量增加
    ++count;
    //唤醒读取时因队列中无元素而导致阻塞的线程
    notEmpty.signal();
    return true;
}

//尾部插入
private boolean linkLast(Node<E> node) {
    
    
    //当前容量大于队列最大容量时,直接返回插入失败
    if (count >= capacity)
        return false;
    //获取尾节点
    Node<E> l = last;
    //将新插入的前一个节点指向原来的尾节点
    node.prev = l;
    //尾结点设置为新插入的结点
    last = node;
    //头结点为空,新插入的结点作为头节点
    if (first == null)
        first = node;
    else
        //将原尾结点的下一个结点指向新插入的节点
        l.next = node;
    //当前容量增加
    ++count;
    //唤醒读取时因队列中无元素而导致阻塞的线程
    notEmpty.signal();
    return true;
}

//头结点插入
public void putFirst(E e) throws InterruptedException {
    
    
    //元素不能为空
    if (e == null) throw new NullPointerException();
    //将元素构造为结点
    Node<E> node = new Node<E>(e);
    //这边会加锁,并调用添加头结点插入的核心方法
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        //头结点如果插入失败,会阻塞该方法,直到取出结点或删除结点时被唤醒
        while (!linkFirst(node))
            notFull.await();
    } finally {
    
    
        //解锁
        lock.unlock();
    }
}

//尾结点插入
public void putLast(E e) throws InterruptedException {
    
    
    //元素不能为空
    if (e == null) throw new NullPointerException();
    //将元素构造为结点
    Node<E> node = new Node<E>(e);
    //这边会加锁,并调用添加尾结点插入的核心方法
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        //尾结点如果插入失败,会阻塞该方法,直到取出结点或删除结点时被唤醒
        while (!linkLast(node))
            notFull.await();
    } finally {
    
    
        lock.unlock();
    }
}

//头结点插入 可指定阻塞时间
public boolean offerFirst(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
    
    
    //元素不能为空
    if (e == null) throw new NullPointerException();
    //将元素构造为结点
    Node<E> node = new Node<E>(e);
    //计算剩余应阻塞时间
    long nanos = unit.toNanos(timeout);
    //这边会加锁,并调用添加头结点插入的核心方法
    final ReentrantLock lock = this.lock;
    //获取可响应中断的锁,保证阻塞时间到期后可重新获得锁
    lock.lockInterruptibly();
    try {
    
    
        //头结点如果插入失败,会阻塞该方法,直到取出结点或删除结点时被唤醒
        //或者阻塞时间到期直接返回失败
        while (!linkFirst(node)) {
    
    
            if (nanos <= 0)
                return false;
            nanos = notFull.awaitNanos(nanos);
        }
        return true;
    } finally {
    
    
        //解锁
        lock.unlock();
    }
}

//头结点插入 可指定阻塞时间
public boolean offerLast(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
    
    
    //元素不能为空
    if (e == null) throw new NullPointerException();
    //将元素构造为结点
    Node<E> node = new Node<E>(e);
    //计算剩余应阻塞时间
    long nanos = unit.toNanos(timeout);
    //这边会加锁,并调用添加尾结点插入的核心方法
    final ReentrantLock lock = this.lock;
    //获取可响应中断的锁,保证阻塞时间到期后可重新获得锁
    try {
    
    
        //尾结点如果插入失败,会阻塞该方法,直到取出结点或删除结点时被唤醒
        //或者阻塞时间到期直接返回失败
        while (!linkLast(node)) {
    
    
            if (nanos <= 0)
                return false;
            nanos = notFull.awaitNanos(nanos);
        }
        return true;
    } finally {
    
    
        //解锁
        lock.unlock();
    }
}

2)出队方法

//删除头结点 - 加锁 直接返回结果
public E pollFirst() {
    
    
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        //调用删除头结点核心方法
        return unlinkFirst();
    } finally {
    
    
        lock.unlock();
    }
}

//删除尾结点 - 加锁 直接返回结果
public E pollLast() {
    
    
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        //调用删除尾结点核心方法
        return unlinkLast();
    } finally {
    
    
        lock.unlock();
    }
}

//删除头结点 - 加锁,如果删除失败则阻塞
public E takeFirst() throws InterruptedException {
    
    
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        E x;
        //调用删除头结点核心方法
        while ((x = unlinkFirst()) == null)
            //阻塞
            notEmpty.await();
        return x;
    } finally {
    
    
        lock.unlock();
    }
}

//删除尾结点 - 加锁,如果删除失败则阻塞
public E takeLast() throws InterruptedException {
    
    
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        E x;
        //调用删除尾结点核心方法
        while ((x = unlinkLast()) == null)
            //阻塞
            notEmpty.await();
        return x;
    } finally {
    
    
        lock.unlock();
    }
}

//删除头结点 - 加锁,如果删除失败则阻塞 可以指定阻塞时间
public E pollFirst(long timeout, TimeUnit unit)
        throws InterruptedException {
    
    
    //计算应阻塞时间
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
    
    
        E x;
        //调用删除头结点核心方法
        while ((x = unlinkFirst()) == null) {
    
    
            if (nanos <= 0)
                //阻塞时间过期,返回结果
                return null;
            //阻塞 并指定阻塞时间
            nanos = notEmpty.awaitNanos(nanos);
        }
        return x;
    } finally {
    
    
        lock.unlock();
    }
}

//删除尾结点 - 加锁,如果删除失败则阻塞 可以指定阻塞时间
public E pollLast(long timeout, TimeUnit unit)
        throws InterruptedException {
    
    
    //计算应阻塞时间
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
    
    
        E x;
        //调用删除尾结点核心方法
        while ((x = unlinkLast()) == null) {
    
    
            if (nanos <= 0)
                //阻塞时间过期,返回结果
                return null;
            //阻塞 并指定阻塞时间
            nanos = notEmpty.awaitNanos(nanos);
        }
        return x;
    } finally {
    
    
        lock.unlock();
    }
}

//删除头结点
private E unlinkFirst() {
    
    
    //获取当前头结点
    Node<E> f = first;
    //头结点为空 返回空
    if (f == null)
        return null;
    //获取头结点的下一个结点
    Node<E> n = f.next;
    //获取头结点元素(记录return需要用到的删除了哪个元素)
    E item = f.item;
    //将头结点元素置为null
    f.item = null;
    //将头结点的下一个结点指向自己 方便gc
    f.next = f;
    //设置头结点为原头结点的下一个结点
    first = n;
    //若原头结点的下一个结点不存在(队列中没有了结点)
    if (n == null)
        //将尾结点也置为null
        last = null;
    else
        //新的头结点的前一个结点指向null,因为他已经作为了头结点 所以不需要指向上一个结点
        n.prev = null;
    //当前数量减少
    --count;
    //唤醒因添加元素时队列容量满导致阻塞的线程
    notFull.signal();
    //返回原来的头结点中的元素
    return item;
}

//删除尾结点
private E unlinkLast() {
    
    
    //获取当前尾结点
    Node<E> l = last;
    //尾结点不存在 返回null
    if (l == null)
        return null;
    //获取当前尾结点的上一个结点
    Node<E> p = l.prev;
    //获取当前尾结点中的元素,需要返回记录
    E item = l.item;
    //将当前尾结点的元素置为null
    l.item = null;
    //并将当前尾结点的上一个结点指向自己,方便gc
    l.prev = l;
    //设置新的尾结点,为原来尾结点的前一个结点
    last = p;
    //若无新的尾结点,头结点置为空(队列中没有了结点)
    if (p == null)
        first = null;
    else
        //将新的尾结点的下一个结点指向null,因为他已经为尾结点所以不需要指向下一个结点
        p.next = null;
    //数量减少
    --count;
    //唤醒因添加元素时队列容量满导致阻塞的线程
    notFull.signal();
    //返回原来的尾结点元素
    return item;
}

LinkedBlockingDeque总结

【1】一个链表阻塞双端无界队列,可以指定容量,默认为 Integer.MAX_VALUE

【2】数据结构:链表(同LinkedBlockingQueue,内部类Node存储元素)

【3】锁:ReentrantLock(同ArrayBlockingQueue)存取是同一把锁,操作的是同一个数组对象

【4】阻塞对象(notEmpty【出队:队列count=0,无元素可取时,阻塞在该对象上】,notFull【入队:队列count=capacity,放不进元素时,阻塞在该对象上】)

扫描二维码关注公众号,回复: 15174588 查看本文章

【5】入队,首尾都可以,均可以添加删除。

【6】出队,首尾都可以,均可以添加删除。

【7】应用场景:常用于 “工作窃取算法”。



一个LinkedBlockingDeque的简单应用

背景:在某产品代码,有事件生成的时候会生成一个事件id,产品会拿事件id进行后面的一系列的逻辑处理。

需求:现在有另外一个需求,也要拿到每一次的事件id,然后在另一个类中处理其他的逻辑。由于原来的逻辑处理较多,新需求只能尽量减少对原代码的嵌入。

思路:创建一个类来处理新需求,这个类提供一个方法A()把事件id存到队列中,在原来产品的代码只需要调A()方法把事件id存到队列,然后就可以在新类中进行逻辑处理。

LinkedBlockingDeque简单介绍( 本文只是简单记录一次使用)

  • LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。
  • 相比于其他阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法
  • LinkedBlockingDeque是可选容量的,默认容量大小为Integer.MAX_VALUE。

使用过程

1.先创建一个CommenServiceImpl类,作为处理新需求的类

@Service("GtmcCommenServiceImpl")
public class CommenServiceImpl{

//省略

}

2.创建LinkedBlockingDeque对象incidentIdList,并新加一个方法来实现往incidentIdList添加事件id

final static BlockingDeque<String> incidentIdList = new LinkedBlockingDeque<>();
private volatile Thread thread = null;
//当有事件id添加成功后,起一个线程单独处理新的逻辑
public void SetIncidentIdList(String incidentId) {
    if (null == incidentId || "".equals(incidentId.trim())) {
        return;
    }
    incidentIdList.offer(incidentId);
    log.info("事件id入List成功:" + incidentId);
    if (thread == null) {
        thread = new Thread(this::setAlarm);
        thread.start();
    }
    if(!thread.isAlive() || thread.isInterrupted()){
        thread = new Thread(this::setAlarm);
        thread.start();
    }
}

3.在原来生成事件id的产品代码注入CommenServiceImpl,然后通过SetIncidentIdList方法添加事件id到LinkedBlockingDeque

gtmcCommenService.SetIncidentIdList(incidentId);

4.在新创建的CommenServiceImpl类处理新的需求逻辑

@Slf4j
@Service("CommenServiceImpl")
public class CommenServiceImpl {
    
    
    protected static ElasticsearchService elasticsearchService;
    @org.springframework.beans.factory.annotation.Value("${douc.ex.accountId}")
    private String accountId;
    @Autowired
    private IncidentService incidentService;
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    @Autowired
    protected EsQueryCreator queryCreator;
    final static BlockingDeque<String> incidentIdList = new LinkedBlockingDeque<>();
  
    private volatile Thread thread = null;
 
    public GtmcCommenServiceImpl() {
    
    
        elasticsearchService = ElasticsearchService.getInstance();
    }
 
    /**
     * 新起一个线程循环处理
     */
    void setAlarm() {
    
    
        while (true) {
    
    
            try {
    
    
                List<String> list = new LinkedList<>();
                incidentIdList.drainTo(list,20);//一次最多从incidentIdList拿20个出来
                for (String evenId : list) {
    
    
                    
 
                   //省略逻辑处理代码
 
 
 
                    kafkaTemplate.send(TOPIC, JSONObject.toJSONString(map));
                    
                }
            } catch (Exception e) {
    
    
                log.error("监控告警生成和解决,发送事件id和配置项id给kafka发生错误 {}" + (e));
            }
        }
    }
 
    public void SetIncidentIdList(String incidentId) {
    
    
        if (null == incidentId || "".equals(incidentId.trim())) {
    
    
            return;
        }
        incidentIdList.offer(incidentId);
        log.info("事件id入List成功:" + incidentId);
        if (thread == null) {
    
    
            thread = new Thread(this::setAlarm);
            thread.start();
        }
        if(!thread.isAlive() || thread.isInterrupted()){
    
    
            thread = new Thread(this::setAlarm);
            thread.start();
        }
    }


LinkedBlockingDeque源码,原理和常用方法,使用场景介绍

概览和简介

LinkedBlockingDeque 是juc 包提供的一个基于链表的双向可选有界的阻塞队列。

通过把参数传给构造参数创建有界的队列可以防止过度的膨胀。没特别指定容量的话,默认最大的容量是 Integer#MAX_VALUE =2^31-1。

链表的每个节点insert时动态创建的。

如果没有阻塞,大多操作能在常数时间完成。除了 #remove(Object) ,removeFirstOccurrence,removeLastOccurrence,contains,iterator 方法和批量操作,这些操作时间复杂度是O(n)。

阻塞队列的特点是,如果队列为空,take/pop一个元素会一直等待(remove 会报错,pull会返回null,poll 看情况,get 会抛异常);如果队列已满,offer/put 一个元素也会阻塞一直等待(add 会错报容量已满)。

数据结构

数据结构本身不是很复杂,首先有一个内部类 final class Node ,用于包装每个节点的数据。另外有成员属性 Node first,Node last,当前拥有元素数量int count ,容量 int capacity;

这下都是常见的,还有用于实现阻塞的 ReentrantLock lock对应的两个Condition,notEmpty和notFull

Node

final 的内部类,只有三个成员变量

E item 当前节点数据
Node<E> prev  指向当前节点前驱节点的地址
Node<E> next  指向当前节点下个节点的地址

常用方法

无参构造方法

无参构造方法返回的对象默认队列长度是Integer的最大值。2^32-1

构造方法 LinkedBlockingDeque(Collection<? extends E> c)

   public LinkedBlockingDeque(Collection<? extends E> c) {
    
    
        this(Integer.MAX_VALUE);
        final ReentrantLock lock = this.lock;
        lock.lock(); // Never contended, but necessary for visibility
        try {
    
    
            for (E e : c) {
    
    
                if (e == null)
                    throw new NullPointerException();
                if (!linkLast(new Node<E>(e)))
                    throw new IllegalStateException("Deque full");
            }
        } finally {
    
    
            lock.unlock();
        }
    }

容量是Integer 最大值

操作前lock.lock()

try {} finally {} 形式释放锁,

取每一个元素,依次放置的链表最后面

添加一个元素 void addFirst(E e)

        if (e == null) throw new NullPointerException();
        Node<E> node = new Node<E>(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
    
    
            return linkFirst(node);
        } finally {
    
    
            lock.unlock();
        }

关键的 linkFirst 方法

    private boolean linkFirst(Node<E> node) {
    
    
        // assert lock.isHeldByCurrentThread();
        if (count >= capacity)
            return false;
        Node<E> f = first;
        node.next = f;
        first = node;
        if (last == null)
            last = node;
        else
            f.prev = node;
        ++count;
        notEmpty.signal();
        return true;
    }

主要逻辑是,先判断当前容量是不是超了,然后把当前节点的的next 指向原来的first,把原first的prev指向当前节点,同时注意如果是第一个元素进来,初始化一下 last 指针。

常规链表操作完,要体现一下阻塞队列的特征,如果有线程在进行类似 take的操作,可能会阻塞等待,所以要用 Condition notEmpty 进行通知,让其可以被唤醒,进行对应操作。

boolean offerFirst(E e)/boolean offerLast(E e)

这个和add方法的区别是,容量满了没加进去,不会直接报错,只会返回 false

putFirst(E e)/putLast(E e)

这两个方法在队列已满的情况下会阻塞,这就是上面方法不同的地方。

    public void putFirst(E e) throws InterruptedException {
    
    
        if (e == null) throw new NullPointerException();
        Node<E> node = new Node<E>(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
    
    
            while (!linkFirst(node))
                notFull.await();
        } finally {
    
    
            lock.unlock();
        }
    }

一个关键的不同就在于,如果尝试linkFirst 失败了,会进行await,等待拿掉一个元素 signal (notify的意思)。

另外有一个点,如果多个线程都在这里await 了,取出一个元素只会signal某一个线程。

boolean offerFirst/offerLast(E e, long timeout, TimeUnit unit)

在队列最前面或者最后面插入一个元素,会等待指定的时间,指定时间后还没成功,就会返回false。

取出一个元素 E removeFirst()/removeLast()

    public E removeFirst() {
    
    
        E x = pollFirst();
        if (x == null) throw new NoSuchElementException();
        return x;
    }

关键方法E unlinkFirst()

    private E unlinkFirst() {
    
    
        // assert lock.isHeldByCurrentThread();
        Node<E> f = first;
        if (f == null)
            return null;
        Node<E> n = f.next;
        E item = f.item;
        f.item = null;
        f.next = f; // help GC
        first = n;
        if (n == null)
            last = null;
        else
            n.prev = null;
        --count;
        notFull.signal();
        return item;
    }

取出其中的第一个,然后标志这个队列肯定已经不是满的,notFull.signal(); 想要 put/offer 可以继续进行了。

E pollFirst()/pollLast()

和remove 方法差不多的,只不多不会抛空指针,会直接返回null

E pollFirst(long timeout, TimeUnit unit)/pollLast(long timeout, TimeUnit unit)

这个重载的方法设计的很奇怪,如果给了超时时间,反而会等待指定的时间,如果到了时间还没取到才会返回null.

E takeFirst()/takeLast()

    public E takeLast() throws InterruptedException {
    
    
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
    
    
            E x;
            while ( (x = unlinkLast()) == null)
                notEmpty.await();
            return x;
        } finally {
    
    
            lock.unlock();
        }
    }

如果没取到会进行一直等待。

E getFirst()/getLast()

这个没什么,取到的为null就抛异常

E peekFirst()/peekLast()

这个对队列本身没有改变,知识对首尾的一次查看操作

    public E peekFirst() {
    
    
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
    
    
            return (first == null) ? null : first.item;
        } finally {
    
    
            lock.unlock();
        }
    }

其他方法就不一一展开了,都比较普通。

应用场景和拓展点

JDK一个提供了7个阻塞队列

  • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
  • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

阻塞队列是典型的生产者消费者模式,和中间件MQ很类似,一边生产一边消费。这样做可以把生产者和消费者进行解耦。

在使用的时候,Node包装的对象,基本上是一个实现Runnable的对象,比如,创建线程池的时候有一个参数就是阻塞队列,用于保存提交过来的任务,然后线程池中的工作线程会不停的从阻塞队列中获取任务进行处理。

猜你喜欢

转载自blog.csdn.net/qq_43842093/article/details/131020301
今日推荐