JDK多线程基础(13):并发数据结构(队列)

并发 Queue

非阻塞队列-ConcurrentLinkedQueue

简介
  1. ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全队列,使用Compare And Swap(CAS)算法实现
  2. 无锁实现,效率比阻塞队列更好
  3. Tomcat 中 NioEndPoint 中的每个 poller 里面就维护一个ConcurrentLinkedQueue<Runnable>用来作为缓冲存放任务
源码分析
  1. 属性: volatile 类型的 Node 节点
  • head:存放链表第一个item为null的节点
  • tail:则并不是总指向最后一个节点
  • Node 节点内部则维护一个变量item用来存放节点的值,next用来存放下一个节点,从而链接为一个单向无界列表
private transient volatile Node<E> head;
private transient volatile Node<E> tail;

public ConcurrentLinkedQueue() {
    head = tail = new Node<E>(null);
}
  1. 方法
  • add:内部调用offer
  • offer:添加数据,添加到尾部(tail)
public boolean offer(E e) {
    checkNotNull(e);    e为null则抛出空指针异常
    
    // 构造Node节点构造函数内部调用unsafe.putObject
    final Node<E> newNode = new Node<E>(e);
    
    // 从尾节点插入
    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        // 如果q=null说明p是尾节点则插入
        if (q == null) {
            // p is last node
            if (p.casNext(null, newNode)) {
                // cas成功说明新增节点已经被放入链表,然后设置当前尾节点(包含head,1,3,5.。。个节点为尾节点)
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        else if (p == q)
            // 多线程操作时候,由于poll时候会把老的head变为自引用,然后head的next变为新head,所以这里需要
            // 重新找新的head,因为新的head后面的节点才是激活的节点
            p = (t != (t = tail)) ? t : head;
        else
            // Check for tail updates after two hops.
            p = (p != t && t != (t = tail)) ? t : q;
    }
}
  • poll:取数据,从头部获取数据(head)并且移除
public E poll() {
    restartFromHead:
 
    // 死循环
    for (;;) {
 
        // 死循环
        for (Node<E> h = head, p = h, q;;) {
 
            // 保存当前节点值
            E item = p.item;
 
            // 当前节点有值则cas变为null(1)
            if (item != null && p.casItem(item, null)) {
                //cas成功标志当前节点以及从链表中移除
                if (p != h) // 类似tail间隔2设置一次头节点(2)
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            // 当前队列为空则返回null(3)
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
            //自引用了,则重新找新的队列头节点(4)
            else if (p == q)
                continue restartFromHead;
            else//(5)
                p = q;
        }
    }
}

final void updateHead(Node<E> h, Node<E> p) {
    if (h != p && casHead(h, p))
        h.lazySetNext(h);
}
  • peek:取数据,从头部获取数据(head),不移除。代码与 poll 类似,只是少了 castItem()

  • size:获取当前队列元素个数,因为使用CAS没有加锁所以从调用size函数到返回结果期间有可能增删元素,导致统计的元素个数不精确

  • isEmpty:都是调用first(),略有不同

Node<E> first() {
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            boolean hasItem = (p.item != null);
            if (hasItem || (q = p.next) == null) {
                updateHead(h, p);
                return hasItem ? p : null;
            }
            else if (p == q)
                continue restartFromHead;
            else
                p = q;
        }
    }
}
  • remove:如果队列里面存在该元素则删除给元素,如果存在多个则删除第一个,并返回true,否者返回 false
简单示例
// 使用 `Compare And Swap`(CAS) 算法实现无锁队列,效率更好
ConcurrentLinkedQueue<String> noBlockQueue = new ConcurrentLinkedQueue<>();

/** 添加方法 */
// add 内部调用 offer 方法
noBlockQueue.add("a");
noBlockQueue.offer("b");
//[a, b]
System.out.println(noBlockQueue);

/** 获取数据的方法 */
// 获取数据,并且从队列移除
System.out.println(noBlockQueue.poll());
System.out.println(noBlockQueue);

// 获取,但是不会从队列移除
System.out.println(noBlockQueue.peek());
System.out.println(noBlockQueue);

// isEmpty 和 size 内部都是调用 first()方法。
// 因为使用CAS没有加锁,所以是一个不精确的数据
System.out.println(noBlockQueue.isEmpty());
System.out.println(noBlockQueue.size());

输出:
[a, b]
a
[b]
b
[b]
false
1

阻塞队列-BlockingQueue

阻塞队列的主要功能其实并不是在于提高并发时的性能,更多的是简化多线程之间的数据共享,如生产者-消费者模式

常用队列
  1. ArrayBlockingQueue:有限队列,底层数组。内部维护定长数组用于缓存对象,维护两个整型常量,分别标识队列头部和尾部在数组中的位置。默认非公平锁
  2. LinkedBlockingQueue:可以设置无限,底层链表。维护一个数据缓冲队列(链表构成)
  3. PriorityBlockingQueue:带有优先级的阻塞队列
常用方法
  1. boolean add(E e):增加一个元索,如果队列已满,则抛出一个异常 IllegalStateException("Queue full")

  2. boolean offer(E e):添加一个元素并返回true,如果队列已满,则返回false。不阻塞

  3. boolean offer(E e, long timeout, TimeUnit unit):队列满时指定阻塞时间,指定时间内还不能加入返回false

  4. void put(E e):添加一个元素,如果队列满,则阻塞,直到有空间

  5. E take():移除并返回队列头部的元素,如果队列为空,则阻塞

  6. E poll(long timeout, TimeUnit unit):取出队首的对象。如果在指定时间内,有数据可取这返回;超时后依然没有数据则返回null

  7. boolean remove(Object o):移除数据

  8. int drainTo(Collection<? super E> c):一次性从队列获取所有可用的数据对象。可以提升获取数据的效率

LinkedBlockingQueue 与 ArrayBlockingQueue 区别
  1. 底层实现不同
  • ArrayBlockingQueue底层是数组
  • LinkedBlockingQueue底层是链表(Node对象)
  1. 队列容量不同
  • ArrayBlockingQueue 是有限队列,构造的时候一定要指定初始化大小。当然如果手动指定其capacity为 `Integer.MAX_VALUE`` 也可以理解为无限
  • LinkedBlockingQueue 默认是无限(Integer.MAX_VALUE),也可以手动缩小其容量,指定capacity
  • 在使用无限队列的过程中,注意当生产大大的快于消费时的 OOM 情况
  1. 同步实现原理不同(锁实现不一样)
  • ArrayBlockingQueue 是一把锁。即其添加与移除操作使用的是同一个 ReenterLock 对象,只是有两个同步 Condition 进行同步。
  • LinkedBlockingQueue 是锁分离,提高并发吞吐量。添加与移除操作是不同的锁,由于链表的缘故,添加和移除操作分别作用于队列的前端和尾端,使用两把不同的锁分离了添加和移除操作。即take与take之间一把锁,put与put之间一把锁
  1. LinkedBlockingQueue 锁分离源码
/** Lock held by take, poll, etc */
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();
  1. ArrayBlockingQueue 一把锁源码
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
....
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull =  lock.newCondition();
简单示例
  1. 添加数据
LinkedBlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(2);

// 1- 增加一个元索,如果队列已满,则抛出一个 IllegalStateException: Queue full 异常
blockingQueue.add("a");
System.out.println(blockingQueue);
// blockingQueue.add("a2");
// IllegalStateException: Queue full
// blockingQueue.add("a3");

// 2-添加一个元素并返回true, 如果队列已满,则返回false,不会有异常
boolean b = blockingQueue.offer("b");
System.out.println(b);
boolean b2 = blockingQueue.offer("b2");
System.out.println(b2);
System.out.println(blockingQueue);

// 3-队列满时指定阻塞时间,在规定时间内如果无法添加则返回false,不会有异常
boolean c = blockingQueue.offer("c", 2, TimeUnit.SECONDS);
System.out.println(c);
boolean c1 = blockingQueue.offer("c1", 2, TimeUnit.SECONDS);
System.out.println(c1);
System.out.println(blockingQueue);

// 清除数据
blockingQueue.clear();

// 4- 添加一个元素,如果队列满,则阻塞
blockingQueue.put("d");
System.out.println(blockingQueue);

输出:
[a]
true
false
[a, b]
false
false
[a, b]
[d]
  1. 取出数据
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(2);

blockingQueue.add("a");
blockingQueue.add("b");

// 1- 移除并返问队列头部的元素, 如果队列为空,则返回null,不会有异常
String poll = blockingQueue.poll();
System.out.println(poll);
System.out.println(blockingQueue);

// 2- 有数据直接返回;队列空时指定阻塞时间,在规定时间内还是没有数据,则返回 null
String poll1 = blockingQueue.poll(2, TimeUnit.SECONDS);
System.out.println(poll1);
String poll2 = blockingQueue.poll(2, TimeUnit.SECONDS);
System.out.println(poll2);
System.out.println(blockingQueue);

blockingQueue.add("a");
blockingQueue.add("b");

// 4-返回但不移除队列头部的元素, 如果队列为空,则返回 null,不会有异常
String peek = blockingQueue.peek();
System.out.println(peek);
System.out.println(blockingQueue);

//        blockingQueue.clear();

// 6-移除并返回队列头部的元素,如果队列为空,则阻塞
blockingQueue.take();
System.out.println(blockingQueue);

blockingQueue.clear();
blockingQueue.add("a");
blockingQueue.add("b");

// 7- 批量获取
ArrayList<String> list = new ArrayList<>();
int i = blockingQueue.drainTo(list);
System.out.println(i);
System.out.println(list);

输出:
a
[b]
b
null
[]
a
[a, b]
[b]
2
[a, b]

Deque(Double-Ended-Queue)双向队列(JDK1.6 以后)

简介

  1. Deque双向队列,允许在队列的头部或者尾部执行出队和入队操作
  2. 与 Queue 相比,具有更加复杂的功能
  3. LinkedListArrayDequeLinkedBlockingDeque 是实现了Deque 接口的常见双向队列。
  • 由于 ArrayDeque 基于数组实现,拥有高效的随机访问性能,但是由于其扩展时需要重新分配内存并进行数组复制,写性能不如 LinkedList
  • LinkedBlockingDeque 是线程安全的双向队列

LinkedBlockingDeque

  1. 使用链表结构,每一个队列节点都维护一个前驱节点和后驱节点
  2. 没有进行读写锁的分离,效率较低

Deque

参考

  1. 源码地址
  2. 非阻塞算法在并发容器中的实现
  3. 并发队列-无界非阻塞队列 ConcurrentLinkedQueue 原理探究

Fork me on Gitee

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值