Java 数据结构集合

news2024/12/24 2:42:08

文章目录

  • Java 数据结构
    • 1. 基本数据结构
      • 1.1 数组 (Array)
      • 1.2 链表 (Linked List)
      • 1.3 栈 (Stack)
      • 1.4 队列 (Queue)
        • 双向队列
        • 优先级队列
    • 2. 树形数据结构
      • 2.1 二叉树 (Binary Tree)
      • 2.2 堆 (Heap)
    • 3. 散列数据结构
      • 3.1 哈希表 (Hash Map)
      • 3.2 LinkedHashMap
      • 3.3 TreeMap
      • ConcurrentHashMap详解
        • ConcurrentHashMap - JDK 1.7
        • ConcurrentHashMap - JDK 1.8
    • 4. 并发数据结构(JUC集合)
      • 4.1 ConcurrentHashMap
      • 4.2 CopyOnWriteArrayList
      • 4.3 ConcurrentLinkedQueue
      • 4.4 BlockingQueue
        • ArrayBlockingQueue:
        • LinkedBlockingQueue:
        • PriorityBlockingQueue:
        • DelayQueue:
        • SynchronousQueue:

Java 数据结构

详细请转到@pdai的博客

1. 基本数据结构

1.1 数组 (Array)

数组的优点:

  • 存取速度快
    数组的缺点:
  • 事先必须知道数组的长度
  • 插入删除元素很慢
  • 空间通常是有限制的
  • 需要大块连续的内存块
  • 插入删除元素的效率很低

源码分析:
1、底层数据结构是Object

 transient Object[] elementData;
 private int size;

2、构造函数包括无参构造和有参数构造,有参构造时指定构造大小,或者直接复制已有的

public ArrayList(int initialCapacity)
public ArrayList()
public ArrayList(Collection<? extends E> c)

其中复制方式,public ArrayList(Collection<? extends E> c),底层实现是通过Arrays.copyof接口来实现的

 elementData = Arrays.copyOf(elementData, size, Object[].class);

3、扩容机制
默认情况下:
ArrayList的容量为10,是由以下参数决定的

private static final int DEFAULT_CAPACITY = 10;

扩容机制触发条件:空间已满
扩容大小:1.5倍
源码调用路径
在这里插入图片描述

其中,在add的时候,会调用 this.ensureCapacityInternal(this.size + 1);其中size+1是当前添加后的容量值,传入ensureExplicitCapacity方法,源码如下:

    private void ensureExplicitCapacity(int minCapacity) {
        ++this.modCount;
        //如果容量不足
        if (minCapacity - this.elementData.length > 0) {
            this.grow(minCapacity);
        }

这里可以看到容量不足时会调用 this.grow(minCapacity);
grow方式是最关键的方法

    private void grow(int minCapacity) {
        int oldCapacity = this.elementData.length;
        //扩容1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        //如果超过这个大容量要单独处理,return minCapacity > 2147483639 ? 2147483647 : 2147483639;
        if (newCapacity - 2147483639 > 0) {
            newCapacity = hugeCapacity(minCapacity);
        }
        //先把之前的copy过去
        this.elementData = Arrays.copyOf(this.elementData, newCapacity);
    }

扩完容之后就可以直接放入数据了

    public boolean add(E e) {
        this.ensureCapacityInternal(this.size + 1);
        this.elementData[this.size++] = e;
        return true;
    }

4、为什么remove性能不好
因为整体要平移,直接看源码

public boolean remove(Object o) {
        int index;
        if (o == null) {
            for(index = 0; index < this.size; ++index) {
                if (this.elementData[index] == null) {
                    this.fastRemove(index);
                    return true;
                }
            }
        } else {
            for(index = 0; index < this.size; ++index) {
                if (o.equals(this.elementData[index])) {
                //效率较高的数组拷贝方式,调用了System.arraycopy,实际上是一种o(n)的复杂度快速改变索引的方式
                    this.fastRemove(index);
                    return true;
                }
            }
        }

        return false;
    }

整体平移的时候,通过一个for循环挨个执行,时间复杂度是o(n),加上this.fastRemove(index);是一种o(n)的拷贝方式,效率可想而知

5、其他API
indexOf(), lastIndexOf():获取元素的第一次出现的index。
get():获取值
set():设置值
addAll():添加大部分数据
6、优化:
在使用ArrayList时如果新增数据,后面逐渐删除,会导致当前数组占用空间过大,无法清理,实际上里面只存储了几个数据的情况,因此,出现这种情况可以利用jdk提供的方法优化:
trimToSize():底层数组的容量调整为当前列表保存的实际元素的大小的功能,源码如下:

 public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
     }
}

1.2 链表 (Linked List)

LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。这样看来,LinkedList简直就是个全能冠军。当你需要使用栈或者队列时,可以考虑使用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue的类(它是个接口名字)。关于栈或队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能。
整体上,这是一个双向链表结构,在Linkedlist定义了三个数据

    transient int size;//当前数目
    transient LinkedList.Node<E> first;//指向链表的第一个
    transient LinkedList.Node<E> last;//指向链表的最后一个元素

这个链表的每个节点的数据结构

Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
this.item = element;//当前元素值
this.next = next;//后驱
this.prev = prev;//前驱
}

1、删除为什么这么快
removeFirst(), removeLast(), remove(e), remove(index)
这些方法用的是

public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

核心方法为unlink,这个方法实际上是改变x这个节点的前驱指向x的后驱,同时将x的指向都变成了null,实际上是个o(1)复杂度的代码

 E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {// 第一个元素
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {// 最后一个元素
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null; // GC
        size--;
        modCount++;
        return element;
    }

2、add为什么不需要扩容
改变指向就好了,可以无限增长
add(E e)、add(int index, E element)
3、get()效率比较低
查找操作的本质是查找元素的下标,如源码所示,会从first遍历整个list,时间复杂度是o(n);

 LinkedList.Node<E> node(int index) {
        LinkedList.Node x;
        int i;
        if (index < this.size >> 1) {
            x = this.first;

            for(i = 0; i < index; ++i) {
                x = x.next;
            }

            return x;
        } else {
            x = this.last;

            for(i = this.size - 1; i > index; --i) {
                x = x.prev;
            }

            return x;
        }
    }

4、其他API
getFirst(), getLast()
获取第一个元素, 和获取最后一个元素:
Queue 方法:
peek():本质上获取第一个元素
poll():本质上将数据从第一个元素删除,并获取第一个元素
remove():调用的是removeFirst()
offer(E e):调用的是add(e)
element():调用的是getFirst()

Deque 方法:
offerFirst(E e):调用addFirst(e)
offerLast(E e):调用addLast(e)
peekFirst(): 获取fist节点的值
peekLast():获取last节点的值
pollFirst():删除第一个节点
pollLast():删除最后一个节点
push(E e):添加作为第一个节点
pop():弹出第一个节点

1.3 栈 (Stack)

栈本质上是一个加了锁的数组(Vector),通过定义规则的方式,实现了先进后出逻辑,但是这个Vector方法由于性能问题已经算是个过时的接口,因此Stack也不再建议使用,这里不再多说,官方更建议将ArrayDeque来作为栈和队列来使用。

1.4 队列 (Queue)

双向队列

Deque是"double ended queue", 表示双向的队列,英文读作"deck". Deque 继承自 Queue接口,除了支持Queue的方法之外,还支持insert, remove和examine操作,由于Deque是双向的,所以可以对队列的头和尾都进行操作,它同时也支持两组格式,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。
当把Deque当做FIFO的queue来使用时,元素是从deque的尾部添加,从头部进行删除的; 所以deque的部分方法是和queue是等同的。具体如下:

    //add调用的方法添加元素到尾部
    public void addLast(E e) {
        if (e == null) {
            throw new NullPointerException();
        } else {
            this.elements[this.tail] = e;
            if ((this.tail = this.tail + 1 & this.elements.length - 1) == this.head) {
                this.doubleCapacity();
            }

        }
    }
    //poll调用的方法,从头部获取元素
    public E pollFirst() {
        int h = this.head;
        E result = this.elements[h];
        if (result == null) {
            return null;
        } else {
            this.elements[h] = null;
            this.head = h + 1 & this.elements.length - 1;
            return result;
        }
    }

ArrayDeque和LinkedList是Deque的两个通用实现,由于官方更推荐使用AarryDeque用作栈和队列,只是底层的数据结构不一样,一个循环数组,一个是双向链表。
在这个循环数组中,为了满足可以同时在数组两端插入或删除元素的需求,数组的任何一点都可能被看作起点或者终点。

优先级队列

这个优先级队列实际上是一个满足堆特性的数组结构。默认情况下是个小顶堆。
可以改为大顶堆

 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());

在介绍PriorityQueue之前首先需要明确树结构与数组结构的关系:

leftNo = parentNo*2+1
rightNo = parentNo*2+2
parentNo = (nodeNo-1)/2

对于数组来说需要了解其初始与扩容机制

int newCapacity = oldCapacity + (oldCapacity < 64 ? oldCapacity + 2 : oldCapacity >> 1);

oldCapacity < 64 判断:如果当前容量小于 64,那么使用 oldCapacity + 2 作为新容量。这是为了在数组容量较小时,提供一些额外的空间,以避免频繁扩容。
oldCapacity >= 64 判断:如果当前容量大于等于 64,那么使用 oldCapacity >> 1(相当于 oldCapacity / 2)作为新容量。这是为了在容量较大时,以较小的步长逐渐扩容,降低扩容的速度。1.5倍

2. 树形数据结构

2.1 二叉树 (Binary Tree)

TreeMap(红黑树的Map结构)

2.2 堆 (Heap)

PriorityQueue,默认小顶堆
可以改为大顶堆

 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());

3. 散列数据结构

3.1 哈希表 (Hash Map)

HashMap实现了Map接口,即允许放入key为null的元素,也允许插入value为null的元素;除该类未实现同步外,其余跟Hashtable大致相同;跟TreeMap不同,该容器不保证元素顺序,根据需要该容器可能会对元素重新哈希,元素的顺序也会被重新打散,因此不同时间迭代同一个HashMap的顺序可能会不同。 根据对冲突的处理方式不同,哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。
resize() 方法用于初始化数组或数组扩容,每次扩容后,容量为原来的 2 倍,并进行数据迁移。
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。
根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

插入:
put(K key, V value)方法是将指定的key, value对添加到map里。该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于getEntry()方法;如果没有找到,则会通过addEntry(int hash, K key, V value, int bucketIndex)方法插入新的entry,插入方式为头插法。
resize() 方法用于初始化数组或数组扩容,每次扩容后,容量为原来的 2 倍,并进行数据迁移。
删除:
remove(Object key)的作用是删除key值对应的entry,该方法的具体逻辑是在removeEntryForKey(Object key)里实现的。removeEntryForKey()方法会首先找到key值对应的entry,然后删除该entry(修改链表的相应引用)。查找过程跟getEntry()过程类似。

获取:
计算 key 的 hash 值,根据 hash 值找到对应数组下标: hash & (length-1)判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步遍历链表,直到找到相等(==或equals)的 key

3.2 LinkedHashMap

LinkedHashMap 是 Java 中的一个集合类,它是 HashMap 的子类。与普通的 HashMap 不同,LinkedHashMap 保留了元素插入的顺序。它使用一个双向链表来维护元素的顺序,该链表连接了所有的元素。因此,当你迭代 LinkedHashMap 时,元素的顺序与插入顺序一致。
1、保留插入顺序: LinkedHashMap 会按照元素插入的顺序来维护元素的顺序,这是通过内部的双向链表实现的。
2、性能类似于 HashMap: 在大多数操作上,LinkedHashMap 的性能与 HashMap 类似。它使用哈希表来实现快速的查找、插入和删除操作。
3、迭代顺序: 迭代 LinkedHashMap 的时候,元素的顺序是按照插入的顺序。这与 HashMap 不同,后者的迭代顺序是不确定的。
4、支持 LRU 缓存策略: LinkedHashMap 提供了一个构造函数,允许你创建一个有限容量的 LinkedHashMap,当达到容量上限时,会根据最近最少使用的原则删除最不经常使用的元素。

3.3 TreeMap

转载TreeMap 源码解析
TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的评判可以通过其本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator)。
TreeMap底层通过红黑树(Red-Black tree)实现,也就意味着containsKey(), get(), put(), remove()都有着log(n)的时间复杂度。

补充:
红黑树:
红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过二者中较低那个的一倍。具体来说,红黑树是满足如下条件的二叉查找树(binary search tree):
1、每个节点要么是红色,要么是黑色。
3、根节点必须是黑色
4、红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
5、对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点。

左旋(Rotate Left):
左旋的过程是将x的右子树绕x逆时针旋转,使得x的右子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。
在这里插入图片描述

左旋代码:

private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right;
        p.right = r.left;
        if (r.left != null)
            r.left.parent = p;
        r.parent = p.parent;
        if (p.parent == null)
            root = r;
        else if (p.parent.left == p)
            p.parent.left = r;
        else
            p.parent.right = r;
        r.left = p;
        p.parent = r;
    }
}

右旋:右旋的过程是将x的左子树绕x顺时针旋转,使得x的左子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。
在这里插入图片描述

TreeMap中右旋代码如下:

private void rotateRight(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> l = p.left;
        p.left = l.right;
        if (l.right != null) l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
            root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}

get方法:
get(Object key)方法根据指定的key值返回对应的value,该方法调用了getEntry(Object key)得到相应的entry,然后返回entry.value。因此getEntry()是算法的核心。算法思想是根据key的自然顺序(或者比较器顺序)对二叉查找树进行查找,直到找到满足k.compareTo(p.key) == 0的entry。

final Entry<K,V> getEntry(Object key) {
    ......
    if (key == null)//不允许key值为null
        throw new NullPointerException();
    Comparable<? super K> k = (Comparable<? super K>) key;//使用元素的自然顺序
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)//向左找
            p = p.left;
        else if (cmp > 0)//向右找
            p = p.right;
        else
            return p;
    }
    return null;
}

put()方法:
put(K key, V value)方法是将指定的key, value对添加到map里。该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于getEntry()方法;如果没有找到则会在红黑树中插入新的entry,如果插入之后破坏了红黑树的约束条件,还需要进行调整(旋转,改变某些节点的颜色)。

public V put(K key, V value) {
	......
    int cmp;
    Entry<K,V> parent;
    if (key == null)
        throw new NullPointerException();
    Comparable<? super K> k = (Comparable<? super K>) key;//使用元素的自然顺序
    do {
        parent = t;
        cmp = k.compareTo(t.key);
        if (cmp < 0) t = t.left;//向左找
        else if (cmp > 0) t = t.right;//向右找
        else return t.setValue(value);
    } while (t != null);
    Entry<K,V> e = new Entry<>(key, value, parent);//创建并插入新的entry
    if (cmp < 0) parent.left = e;
    else parent.right = e;
    fixAfterInsertion(e);//调整
    size++;
    return null;
}

上述代码的插入部分并不难理解: 首先在红黑树上找到合适的位置,然后创建新的entry并插入(当然,新插入的节点一定是树的叶子)。难点是调整函数fixAfterInsertion(),前面已经说过,调整往往需要1.改变某些节点的颜色,2.对某些节点进行旋转。

ConcurrentHashMap详解

JDK1.7之前的ConcurrentHashMap使用分段锁机制实现,JDK1.8则使用数组+链表+红黑树数据结构和CAS原子操作实现ConcurrentHashMap;本文将分别介绍这两种方式的实现方案及其区别。

ConcurrentHashMap - JDK 1.7

ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可。因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作。
Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写。

初始化:
loadFactor: 负载因子,是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,插入第二个会进行第一次扩容
initialCapacity: 初始容量

并发问题安全性:
put 操作的线程安全性。
使用了 CAS 来初始化 Segment 中的数组。添加节点到链表的操作是插入到表头的,所以,如果这个时候 get 操作在链表遍历的过程已经到了中间,是不会影响的。
当然,另一个并发问题就是 get 操作在 put 之后,需要保证刚刚插入表头的节点被读取,这个依赖于 setEntryAt 方法中使用的 UNSAFE.putOrderedObject。
扩容。扩容是新创建了数组,然后进行迁移数据,最后面将 newTable 设置给属性 table。所以,如果 get 操作此时也在进行,那么也没关系,如果 get 先行,那么就是在旧的 table 上做查询操作;而 put 先行,那么 put 操作的可见性保证就是 table 使用了 volatile 关键字。
remove 操作的线程安全性。
get 操作需要遍历链表,但是 remove 操作会"破坏"链表。如果 remove 破坏的节点 get 操作已经过去了,那么这里不存在任何问题。如果 remove 先破坏了一个节点,分两种情况考虑。
1、如果此节点是头节点,那么需要将头节点的 next 设置为数组该位置的元素,table 虽然使用了 volatile 修饰,但是 volatile 并不能提供数组内部操作的可见性保证,所以源码中使用了 UNSAFE 来操作数组,请看方法 setEntryAt。
2、如果要删除的节点不是头节点,它会将要删除节点的后继节点接到前驱节点中,这里的并发保证就是 next 属性是 volatile 的。

ConcurrentHashMap - JDK 1.8

在JDK1.8中,ConcurrentHashMap选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。

4. 并发数据结构(JUC集合)

4.1 ConcurrentHashMap

4.2 CopyOnWriteArrayList

CopyOnWriteArrayList实现了List接口,List接口定义了对列表的基本操作;同时实现了RandomAccess接口,表示可以随机访问(数组具有随机访问的特性);同时实现了Cloneable接口,表示可克隆;同时也实现了Serializable接口,表示可被序列化。

CopyOnWriteArrayList 是 Java 中并发编程提供的线程安全的集合类之一,它的特点是在进行写操作时会创建一个新的复制(copy)来保证线程安全。以下是 CopyOnWriteArrayList 的主要原理和特点:
1、写时复制: 当有写操作(如添加、删除元素)发生时,CopyOnWriteArrayList 会创建一个新的数组(复制原数组),在新数组上执行写操作,然后将新数组替换原来的数组。这样可以确保写操作不会影响正在进行的读操作,因为读操作仍然在原数组上进行。
2、读操作不需要加锁: 由于读操作不涉及修改集合内容,而是在不变的原数组上进行,因此读操作不需要加锁。这使得在读多写少的场景中,CopyOnWriteArrayList 的性能相对较好。
3、适用于读多写少的场景: CopyOnWriteArrayList 的设计适用于那些读操作频繁,而写操作相对较少的场景。这是因为在写时需要进行数组复制,可能会引起一些性能开销。
4、弱一致性: CopyOnWriteArrayList 提供的是弱一致性,即在写操作完成后,对于读操作来说,并不一定能立即看到最新的修改。这是因为读操作仍然可能在旧数组上进行,直到写操作完成并新数组替换了旧数组。
5、不支持并发修改的迭代器: 由于写操作会创建新的数组,旧数组上的迭代器可能会抛出 ConcurrentModificationException 异常。因此,CopyOnWriteArrayList 的迭代器是不支持并发修改的。

CopyOnWriteArrayList的缺陷和使用场景CopyOnWriteArrayList 有几个缺点:由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

4.3 ConcurrentLinkedQueue

ConcurerntLinkedQueue一个基于链接节点的无界线程安全队列。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue是一个恰当的选择。此队列不允许使用null元素。

ConcurrentLinkedQueue 是 Java 中并发编程提供的线程安全的队列实现。它基于非阻塞算法,使用 CAS(Compare and Swap)等原子操作来确保线程安全。以下是 ConcurrentLinkedQueue 的主要特点和原理:
非阻塞算法: ConcurrentLinkedQueue 使用非阻塞算法,避免了使用锁的情况,因此具有较好的并发性能。它通过 CAS 操作等方式来保证多个线程可以同时进行读和写操作。

1、无界队列: ConcurrentLinkedQueue 是无界队列,它没有固定的容量限制。可以不受限制地添加元素。
2、基于链表: 内部使用单向链表来组织队列元素。新的元素总是被添加到队列的尾部,而移除元素则发生在队列的头部。
3、线程安全的入队和出队操作: ConcurrentLinkedQueue 提供了线程安全的入队(offer和add)和出队(poll和remove)操作。这意味着多个线程可以同时进行这些操作而不需要额外的同步。
4、迭代器支持弱一致性: ConcurrentLinkedQueue 的迭代器是弱一致性的,它不会抛出 ConcurrentModificationException 异常。因此,迭代器可能看到队列的部分修改,而不一定是最新状态。
5、适用于生产者-消费者模型: ConcurrentLinkedQueue 在生产者-消费者模型中是一个常用的选择,特别是当多个线程并发地生产和消费元素时。

4.4 BlockingQueue

BlockingQueue大家族有哪些? ArrayBlockingQueue, DelayQueue, LinkedBlockingQueue, SynchronousQueue…
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。
负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。

ArrayBlockingQueue:

基于数组的阻塞队列实现。
有界队列,必须指定容量。
使用独占锁实现线程安全。

BlockingQueue<Integer> arrayBlockingQueue = new ArrayBlockingQueue<>(10);
LinkedBlockingQueue:

基于链表的阻塞队列实现。
可以选择有界或无界。
如果不指定容量,默认是无界队列。
使用两个锁(读锁和写锁)实现线程安全。

BlockingQueue<String> linkedBlockingQueue = new LinkedBlockingQueue<>();
PriorityBlockingQueue:

一个支持优先级的无界阻塞队列。
元素按照它们的自然顺序或者根据构造函数中提供的比较器来进行排序。
不保证同优先级元素的顺序。

BlockingQueue<Task> priorityBlockingQueue = new PriorityBlockingQueue<>();
DelayQueue:

用于存放实现了 Delayed 接口的元素,这些元素只能在其指定的延迟时间过后才能被消费。
通常用于定时任务调度。

BlockingQueue<DelayedTask> delayQueue = new DelayQueue<>();
SynchronousQueue:

一个不存储元素的阻塞队列。
生产者线程必须等待消费者线程取走元素,反之亦然。
用于直接传递任务或数据,通常在生产者和消费者线程之间进行交互。

BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1407611.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

pytorch(二)梯度下降算法

文章目录 优化问题梯度下降随机梯度下降 在线性模型训练的时候&#xff0c;一开始并不知道w的最优值是什么&#xff0c;可以使用一个随机值来作为w的初始值&#xff0c;使用一定的算法来对w进行更新 优化问题 寻找使得目标函数最优的权重组合的问题就是优化问题 梯度下降 通…

Nginx问题分析

问题再现 分析问题&#xff1a; 就是通过http://182.44.16.68:8077/web-ui/static/js/chunk-libs.82635094.js 地址访问&#xff0c;找不到对应的js文件 首先确认文件在服务器的位置 发现这个目录下确实有这个js文件&#xff0c;那问题就在于http://182.44.16.68:8077/web-ui…

225.用队列实现栈(附带源码)

目录 一、思路 二、源码 一、思路 所以&#xff0c;创建两个队列 入栈&#xff0c;那个不空入那个 出栈&#xff0c;移动不空的队列的前n-1个到空队列&#xff0c;出队列第n个 很简单 总的来说&#xff0c;就是 下面直接手撕代码&#xff1a; 二、源码 typedef int QDa…

Unity_使用Image和脚本生成虚线段

生成如图样式的虚线段 原理&#xff1a;使用Image做一条线段&#xff0c;这个方法的原理就是给固定的片元长度&#xff0c;对Image进行分割&#xff0c;把片元添加到一个列表中&#xff0c;然后循环对列表中的偶数位进行隐藏&#xff0c;也可以调整线段的宽度 缺陷&#xff1…

Pandas.Series.idxmin() 最小值索引 详解 含代码 含测试数据集 随Pandas版本持续更新

关于Pandas版本&#xff1a; 本文基于 pandas2.2.0 编写。 关于本文内容更新&#xff1a; 随着pandas的stable版本更迭&#xff0c;本文持续更新&#xff0c;不断完善补充。 传送门&#xff1a; Pandas API参考目录 传送门&#xff1a; Pandas 版本更新及新特性 传送门&…

Java Web(二)--HTML

基本介绍 官网文档地址: HTML 教程 HTML&#xff08;HyperText Mark-up Language&#xff09;即超文本标签语言&#xff1b;HTML 文本是由 HTML 标签组成的文本&#xff0c;可以包括文字、图形、动画、声音、表格、链接等&#xff1b;HTML 的结构包括头部&#xff08;Head&…

如何通过内网穿透+代理共享网络

去年写了一篇博客&#xff1a;如何通过代理共享网络&#xff0c;在这篇文章探索了怎么在同一个局域网内共享代理服务。不过&#xff0c;它的实用性也比较缺乏&#xff0c;要求必须处于同一个局域网之下&#xff0c;大多数时候&#xff0c;我们可能很难有这样的环境。所以&#…

【GitHub项目推荐--12 年历史的 PDF 工具开源了】【转载】

最近在整理 PDF 的时候&#xff0c;有一些需求普通的 PDF 编辑器没办法满足&#xff0c;比如 PDF 批量合并、编辑等。 于是&#xff0c;我就去 GitHub 上看一看有没有现成的轮子&#xff0c;发现了这个 PDF 神器「PDF 补丁丁」&#xff0c;让人惊讶的是这个 PDF 神器有 12 年的…

基于SpringBoot Vue美食网站系统

大家好✌&#xff01;我是Dwzun。很高兴你能来阅读我&#xff0c;我会陆续更新Java后端、前端、数据库、项目案例等相关知识点总结&#xff0c;还为大家分享优质的实战项目&#xff0c;本人在Java项目开发领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#x…

mysql生成最近24小时整点/最近30天/最近12个月时间临时表

文章目录 生成最近24小时整点生成最近30天生成最近12个月 生成最近24小时整点 SELECT-- 每向下推1行, i比上次减去1b.*, i.*,DATE_FORMAT( DATE_SUB( NOW(), INTERVAL ( -( i : i - 1 ) ) HOUR ), %Y-%m-%d %H:00 ) AS time FROM-- 目的是生成12行数据( SELECTa FROM( SELECT…

可直接将视频转文字的工具,速到快到离谱!

如何将视频转换成文字&#xff0c;推荐大家使用视频提取文案小助手&#xff0c;三秒一键搞定&#xff0c;真的快到离谱​&#xff01; 不少草根博主在做短视频的时候&#xff0c;就有很多人给大家支招让大家先模仿后超越的模式&#xff0c;激起一众爱好短视频的草根博主成为短…

Scrapy爬虫在新闻数据提取中的应用

Scrapy是一个强大的爬虫框架&#xff0c;广泛用于从网站上提取结构化数据。下面这段代码是Scrapy爬虫的一个例子&#xff0c;用于从新闻网站上提取和分组新闻数据。 使用场景 在新闻分析和内容聚合的场景中&#xff0c;收集和组织新闻数据是常见需求。例如&#xff0c;如果我…

【小黑嵌入式系统第十六课】PSoC 5LP第三个实验——μC/OS-III 综合实验

上一课&#xff1a; 【小黑嵌入式系统第十五课】μC/OS-III程序设计基础&#xff08;四&#xff09;——消息队列&#xff08;工作方式&数据通信&生产者消费者模型&#xff09;、动态内存管理、定时器管理 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂…

纯注解开发bean

注解开发定义bean&#xff1a;Controller:用于表现层bean定义&#xff1b;Service&#xff1a;用于业务层bean定义&#xff1b;Repository&#xff1a;用于数据层bean定义。 我们先来完成数据层和业务逻辑层的注解 数据层&#xff1a; package org.example.dao.impl;import or…

智慧博物馆信息化系统建设(3)

博物馆智能电子导览系统 IPAD智能化定制服务 系统采用的IPAD。使用者通过智能IPAD终端上的三维立体导图,可以在参观的同时,随时读取展馆平面地图以及展品相关信息,然后选择相关服务。简单操作便可获得文字、图片、声音以及视频资料展现给使用者。 游客通过该智能IPAD终端…

构建中国人自己的私人GPT—与文档对话

先看效果 他可以从上传的文件中提取内容作为答案。上传文件摄取速度 摄取速度取决于您正在摄取的文档数量以及每个文档的大小。为了加快摄取速度&#xff0c;您可以在配置中更改摄取模式。 存在以下摄取模式&#xff1a; simple&#xff1a;历史行为&#xff0c;一次按顺序摄…

03 SpringBoot实战 -微头条之首页门户模块(跳转某页面自动展示所有信息+根据hid查询文章全文并用乐观锁修改阅读量)

1.1 自动展示所有信息 需求描述: 进入新闻首页portal/findAllType, 自动返回所有栏目名称和id 接口描述 url地址&#xff1a;portal/findAllTypes 请求方式&#xff1a;get 请求参数&#xff1a;无 响应数据&#xff1a; 成功 {"code":"200","mes…

RubbleDB: CPU-Efficient Replication with NVMe-oF——论文泛读

ATC 2023 Paper 论文阅读笔记整理 问题 由于需要执行昂贵的后台压缩操作&#xff0c;CPU 往往是持久键值存储的性能瓶颈。在日志结构合并树&#xff08;LSM树&#xff09;&#xff0c;标准的基于磁盘的键值存储设计[2,4,8,22,41]&#xff0c;压缩可以在生产工作负载中消耗高达…

基于FPGA的OFDM基带发射机的设计与实现

文章目录 前言一、OFDM描述二、本系统的实现参照 1.IEEE 802.11a协议主要参数2.不同调制方式与速率 3. IFFT映射关系4. IEEE 802.11a物理层规范5. PPDU帧格式三、设计与实现 1.扰码2.卷积编码与删余3.数据交织4.符号调制5.导频插入6.IFFT变换 7.循环前缀&加窗8.训练序列生成…

快速上手的AI工具-文心一言绘本创作

前言 大家好晚上好&#xff0c;现在AI技术的发展&#xff0c;它已经渗透到我们生活的各个层面。对于普通人来说&#xff0c;理解并有效利用AI技术不仅能增强个人竞争力&#xff0c;还能在日常生活中带来便利。无论是提高工作效率&#xff0c;还是优化日常任务&#xff0c;AI工具…