• 如何使用ConcurrentLinkedQueue做一个缓存队列


    在项目中,我们可能会用到队列,那么ConcurrentLinkedQueue,是必不可少的一个。对于多线程环境下,想要实现线程安全,要么自己加锁,要么利用线程安全队列-------ConcurrentLinkedQueue。

    先看一下使用ConcurrentLinkedQueue是如何做一个缓存队列的。

    package server.utils;
    
    import java.util.concurrent.ConcurrentLinkedQueue;
    
    public class QueueUtils {
    
        //用户缓存队列
        private static final ConcurrentLinkedQueue<String> queueCache = new ConcurrentLinkedQueue<>();
    
    
        private int rank;
    
        //入队
        public void offer(String userId) {
            queueCache.offer(userId);
        }
    
        //判断元素是否存在
        public boolean isExist(String userId) {
            if (queueCache.contains(userId)) {
                return true;
            }
            return false;
        }
    
        //获取排名
        public int getRank(String userId) {
            if (isExist(userId)) {
                for (String id : queueCache) {
                    if (id.equals(userId)) {
                        break;
                    }
                    rank++;
                }
                System.out.printf("rank排名为" + rank);
                return rank;
            } else {
                //不包括则加入队尾
                offer(userId);
                rank = queueCache.size() + 1;
                System.out.printf("不包含用户则加入队尾" + rank);
                return rank;
            }
        }
    
        public static void main(String[] args) {
            for (int i = 1; i < 6; i++) {
                new QueueUtils().getRank("1" + i);
            }
            new QueueUtils().getRank("15");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    在这里插入图片描述

    总结一下常用的三个方法:peak(),poll()方法,offer()方法

    peak()方法:

    这个方法的作用就是获取队列头部的元素,只获取不移除,注意这个方法和下面的poll方法的区别啊!

    public E peek() {
        //[1]goto标志
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                //[2]
                E item = p.item;
                //[3]
                if (item != null || (q = p.next) == null) {
                    updateHead(h, p);
                    return item;
                }
                //[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); 
           }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    poll()方法:

    这个方法是获取头部的这个节点,如果队列为空则返回null;

    public E poll() {
        //这里其实就是一个goto的标记,用于跳出for循环
        restartFromHead:
        //[1]
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;
                //[2]如果当前节点中存的值不为空,则CAS设置为null
                if (item != null && p.casItem(item, null)) {
                    //[3]CAS成功就更新头节点的位置
                    if (p != h) 
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                //[4]当前队列为空,就返回null
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                //[5]当前节点和下一个节点一样,说明节点自引用,则重新找头节点
                else if (p == q)
                    continue restartFromHead;
                //[6]
                else
                    p = q;
            }
        }
    }
    
    final void updateHead(Node<E> h, Node<E> p) {
        if (h != p && casHead(h, p))
            h.lazySetNext(h);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    offer()方法:

    这个方法的作用就是在队列末端添加一个节点,如果传递的参数是null,就抛出空指针异常,否则由于该队列是无界队列,该方法会一直返回true,而且该方法使用CAS算法实现的,所以不会阻塞线程

    //队列末端添加一个节点
    public boolean offer(E e) {
        //如果e为空,那么抛出空指针异常
        checkNotNull(e);
        //将传进来的元素封装成一个节点,Node的构造器中调用UNSAFE.putObject(this, itemOffset, item)把e赋值给节点中的item
        final Node<E> newNode = new Node<E>(e);
    
        //[1]
        //这里的for循环从最后的节点开始
        for (Node<E> t = tail, p = t;;) {
          Node<E> q = p.next;
          //[2]如果q为null,说明p就是最后的节点了
            if (q == null) {
                //[3]CAS更新:如果p节点的下一个节点是null,就把写个节点更新为newNode
                if (p.casNext(null, newNode)) {
                    //[4]CAS成功,但是这时p==t,所以不会进入到这里的if里面,直接返回true
                    //那么什么时候会走到这里面来呢?其实是要有另外一个线程也在调用offer方法的时候,会进入到这里面来
                    if (p != t) 
                        casTail(t, newNode);  
                    return true;
                }
            }
            else if (p == q) //[5]
                
                p = (t != (t = tail)) ? t : head;
            else //[6]
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    总结:

    其实还有几个方法没说,但是感觉比较容易就不浪费篇幅了,有兴趣的可以看看:size方法用于计算队列中节点的数量,可是由于没有加锁,在并发的条件下不准确;remove方法删除某个节点,其实就是遍历然后用equals方法比较item是不是一样,只不过如果存在多个符合条件的节点只删除第一个,然后返回true,否则返回false;contains方法判断队列中是否包含指定item的节点,也就是遍历,很容易;

    最麻烦的就是offer方法和poll方法,offer方法是在队列的最后面添加节点,而poll是获取头节点,并且删除第一个真正的队列节点(注意,节点分为两种,一种是哨兵节点,一种是真正的存了数据的节点啊),还简单的说了一下poll方法和peek方法的区别,后者只是获取,而不删除啊!

  • 相关阅读:
    Oracle database oracle12c RAC 增加PDB
    商用车第一张,比亚迪引领汽车智能网联安全合规新趋势
    子不语IPO下限定价:预计2022年全年净利润下滑,华丙如为实控人
    npmp 的简单理解
    汇编语言实验3:DEBUG的使用
    CSS3_字体图标
    C#中.NET 7.0 Windows窗体应用通过EF访问新建数据库
    常用的辅助网站(持续更新)
    Jetpack Compose基础组件之 — Text
    【LeetCode】No.108. Convert Sorted Array to Binary Search Tree -- Java Version
  • 原文地址:https://blog.csdn.net/weixin_44427181/article/details/125603180