Java队列之LinkedBlockingQueue源码解析

31 篇文章 0 订阅
16 篇文章 2 订阅

目录

1.LinkedBlockingQueue

1.1整体架构

1.2初始化源码解析

1.3阻塞新增源码解析

1.4阻塞删除源码分析

1.5查看元素源码分析


1.LinkedBlockingQueue


1.1整体架构

  • 主要实现了BlockingQueue和Queue接口
  • Queue接口包含了:
遇到队列满或空的时候,抛异常,如 add、remove、element;
遇到队列满或空的时候,返回特殊值,如 offer、poll、peek。
  • BlockingQueue接口包含了:
遇到队列满或空的时候,抛异常,如 add、remove、element;
遇到队列满或空的时候,返回特殊值/阻塞一段时间如 offer、poll、peek(不会进行阻塞,直接返回);
遇到队列满或空的时候,一直阻塞,如 put、take;
  • 底层数据结构使用链表,链表大小可以在初始化进行设置,默认是int的最大值
// 链表结构 begin
//链表的元素
static class Node<E> {
    E item;
    //当前元素的下一个,为空表示当前节点是最后一个
    Node<E> next;
    Node(E x) { item = x; }
}
//链表的容量,默认 Integer.MAX_VALUE
private final int capacity;
//链表已有元素大小,使用 AtomicInteger,所以是线程安全的
private final AtomicInteger count = new AtomicInteger();
//链表头
transient Node<E> head;
//链表尾
private transient Node<E> last;
// 链表结构 end

// 锁 begin
//take 时的锁
private final ReentrantLock takeLock = new ReentrantLock();
// take 的条件队列,condition 可以简单理解为基于 ASQ 同步机制建立的条件队列
private final Condition notEmpty = takeLock.newCondition();
// put 时的锁,设计两把锁的目的,主要为了 take 和 put 可以同时进行
private final ReentrantLock putLock = new ReentrantLock();
// put 的条件队列
private final Condition notFull = putLock.newCondition();
// 锁 end

// 迭代器 
// 实现了自己的迭代器
private class Itr implements Iterator<E> {
………………
}

1.2初始化源码解析

// 不指定容量,默认 Integer 的最大值
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE); //调用指定容量初始化
}

// 指定链表容量大小,链表头尾相等,节点值(item)都是 null
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) 
    this.capacity = capacity;
    // 头节点和尾节点永远不为空,会有一个head节点一直会指向这个哨兵节点!!!
    last = head = new Node<E>(null);
}

// 已有集合数据进行初始化
public LinkedBlockingQueue(Collection<? extends E> c) {
    this(Integer.MAX_VALUE);//调用指定容量初始化
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // 获取锁
    try {
        int n = 0;
        for (E e : c) {
            // 集合内的元素不能为空
            if (e == null)
                throw new NullPointerException();
            // capacity 代表链表的大小,在这里是 Integer 的最大值
            // 如果集合类的大小大于 Integer 的最大值,就会报错
            // 其实这个判断完全可以放在 for 循环外面,这样可以减少 Integer 的最大值次循环(最坏情况)
            if (n == capacity)
                throw new IllegalStateException("Queue full");
            enqueue(new Node<E>(e));
            ++n;
        }
        count.set(n);
    } finally {
        putLock.unlock();
    }
}

第一种:不指定初始容量默认为int的最大值

第二种:指定初始值大小,

第三种:指定集合来进行初始化,需要加put锁来保证线程安全性

ps:三种初始化其实都是指定大小的初始化,只不过进行了封装,头节点永远的会指向一个空值的哨兵节点,可以简化编程的复杂程度!!!

1.3阻塞新增源码解析

// 把e新增到队列的尾部。
public void put(E e) throws InterruptedException {
    // e 为空,抛出异常
    if (e == null) throw new NullPointerException();
    // 预先设置 c 为 -1,约定负数为新增失败
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock; //获取锁
    final AtomicInteger count = this.count; //获取节点值
    putLock.lockInterruptibly(); // 可中断锁
    try {
        // 队列满了
        // 当前线程阻塞,等待其他线程的唤醒(其他线程 take 成功后一定会唤醒此处被阻塞的线程)
        while (count.get() == capacity) {
            // await 无限等待
            notFull.await();
        }
        // 队列没有满,直接新增到队列的尾部
        enqueue(node);
        // 新增计数赋值,注意这里 getAndIncrement 返回的是旧值
        // 这里的 c 是比真实的 count 小 1 的
        c = count.getAndIncrement();
        // 如果链表现在的大小 小于链表的容量,说明队列未满
        // 可以尝试唤醒一个 put 的等待线程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        // 释放锁
        putLock.unlock();
    }
    // c==0,代表队列里面有一个元素,会尝试唤醒一个take的等待线程
    if (c == 0)
        signalNotEmpty();
}
// 入队,把新元素放到队尾
private void enqueue(Node<E> node) {
    last = last.next = node; //很简洁,第二个等号赋值后会返回node的值
}

获取到put锁,获取到原子类型count(底层使用的CAS操作),并且设置锁可中断,如果队列已满就会进行无期限等待(需要手动的进行唤醒)在进行尾部添加节点并且累加count值,如果链表没满会尝试唤醒一个put线程,然后释放锁。

最后在判断增加后队列是否只有一个值,如果是的话需要进行唤醒一个take线程

ps:尾部添加元素由于有了哨兵节点(head节点)变得非常优雅简介!否则是需要判断空链表,进行更新head节点的。

1.4阻塞删除源码分析

// 阻塞拿数据
public E take() throws InterruptedException {
    E x;
    // 默认负数,代表失败
    int c = -1;
    // count 代表当前链表数据的真实大小
    final AtomicInteger count = this.count;// 获取原子类型进行操作
    final ReentrantLock takeLock = this.takeLock;// 获取take锁
    takeLock.lockInterruptibly();// 可中断锁
    try {
        // 空队列时,会被阻塞,等待其他线程唤醒
        while (count.get() == 0) {
            notEmpty.await();
        }
        // 非空队列,从队列的头部拿一个出来
        x = dequeue();
        // 原子操作减一,返回的值是旧值
        c = count.getAndDecrement();
        // 如果队列里面有值,从 take 的等待线程里面唤醒一个。
        if (c > 1)
            notEmpty.signal();
    } finally {
        // 释放锁
        takeLock.unlock();
    }
    // 已经删除了一个元素,
    // 如果队列空闲还剩下一个,尝试从 put 的等待线程中唤醒一个
    if (c == capacity)
        signalNotFull();
    return x;
}
// 队头中取数据
// 每次更新的实际是哨兵节点,是哨兵节点不停地在移动
// 把原本的哨兵节点变为循环引用,然后更新哨兵为下一个元素,并将此节点变为哨兵节点
private E dequeue() {
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // 当前哨兵节点变为循环引用,help GC
    head = first;
    E x = first.item;// 返回删除节点的元素
    first.item = null;// 更新下一个节点为哨兵节点
    return x;
}

整个过程和put方法大致类似,取头节点较为复杂(每次实际为哨兵节点在不停的移动)

获取take锁,获取原子类型队列的count,进行加锁并且设置锁为可中断,如果队列为空会无限阻塞(直到有人唤醒),然后从头节点获取值并且删除该节点,如果队列还有值会尝试唤醒一个take线程,然后释放锁,

如果队列在取元素之前是满的那么会尝试唤醒一个put线程,最后返回节点的值。

1.5查看元素源码分析

// 查看并不删除元素,如果队列为空,返回 null
public E peek() {
    // count 代表队列实际大小,队列为空,直接返回 null
    if (count.get() == 0)
        return null;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        // 拿到队列头
        Node<E> first = head.next;
        // 判断队列头是否为空,并返回
        // 不会被进行阻塞
        if (first == null)
            return null;
        else
            return first.item;
    } finally {
        takeLock.unlock();
    }
}

只是取元素但不进行删除,还是会获取take锁防止节点被改变,主要就是进行了判断队列的空条件,不会被阻塞!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值