java 集合源码分析2

LinkedHashMap 源码分析

LinkedHashMap 简介?

  • LinkedHashMap 继承自 HashMap,实现了 Map 接口,并且在 HashMap 的基础上维护了一个双向链表;
  • 支持遍历时,按照插入的顺序进行有序遍历;
  • 支持按照元素的访问顺序排序,适用于封装 LRU (最近最少使用)缓存工具;
  • 因为内部使用双向链表维护各个节点,所以遍历效率和元素个数成正比,相较于遍历效率和容量成正比的 HashMap 来说,跌打效率会高很多;

如何使用 LinkedHashMap 的按照访问顺序排序?

  • LinkedHashMap 定义了排序模式 accessOrder (boolean 类型,默认为 false),按照访问顺序排序则为 true,按照插入顺序排序则为 false;
  • 要实现访问顺序遍历,我们可以使用传入 accessOrder 属性的 LinkedHashMap 构造方法,并将 accessOrder 设置为 true,使其具备访问有序性,每次被 get 的元素都会重新排序到链表的尾部;
java 复制代码
Map<String, String> map = new LinkedHashMap<>(16,0.75f, true);

如何使用 LinkedHashMap 实现一个 LRU 缓存?

  • 继承 LinkedHashMap;
  • 构造方法中指定 accessOrder 为 true ,这样在访问元素时就会把该元素移动到链表尾部,链表首元素就是最近最少被访问的元素;
  • 重写 removeEldestEntry 方法,该方法会返回一个 boolean 值,告知 LinkedHashMap 是否需要移除链表首元素(缓存容量有限)。
java 复制代码
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    /**
     * 判断size超过容量时返回true,告知LinkedHashMap移除最老的缓存项(即链表的第一个元素)
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

LinkedHashMap.Entry 与 HashMap.Node 及 HashMap.TreeNode 的关系?以及为什么要这样设计?

  • LinkedHashMap.Entry 继承自 HashMap.Node 并添加了 before 和 after 两个指针来实现插入\访问有序性的;
  • HashMap.TreeNode 又继承自 LinkedHashMap.Entry,并添加了 parent、left、right、prev 指针及颜色标识 red;
  • 对于 LinkedHashMap.Entry 继承自 HashMap.Node 是为了复用;
  • HashMap.TreeNode 又继承自 LinkedHashMap.Entry,没找到好的理由;

LinkedHashMap 的构造函数?

LinkedHashMap 一共有四个构造函数,默认情况下都是调用父类的构造函数完成初始化,并且按照访问顺序排队默认是 false,除非在构造函数中指定为 true;

get 方法源码详解?

java 复制代码
public V get(Object key) {
     Node < K, V > e;
     //获取key的键值对,若为空直接返回;
     if ((e = getNode(hash(key), key)) == null)
         return null;
     //若accessOrder为true,则调用afterNodeAccess将当前元素移到链表末尾;
     if (accessOrder)
         afterNodeAccess(e);
     //返回键值对的值
     return e.value;
 }

afterNodeAccess 方法源码详解?

  1. 首先判断 accessOrder 并且当前节点不是 tail 点;
  2. 获取当前节点,以及当前的前置和后驱节点;
  3. 将当前节点的后继指针置空;
  4. 判断前驱节点是否为 null,如果为 null,说明当前节点是 head 点,需要把后继节点赋值给 head 点,否则的话把前驱节点的后继指针指向后继节点;
  5. 判断后继节点,如果后继节点不为 null,则把后继节点的前驱指针指向前驱节点,否则说明后驱节点是 tail 点,把后继节点赋值给 last;
  6. 最后判断 last 是否为 null,如果为 null 说明当前只有一个节点,直接把当前节点赋值给 head 点,否则把当前节点维护到 last 之后,最后把当前节点赋值给 tail 点;
java 复制代码
// 将访问过的节点移动到链表结尾
void afterNodeAccess(Node < K, V > e) {
    LinkedHashMap.Entry < K, V > last;
    //如果 accessOrder 且当前节点不未链表尾节点
    if (accessOrder && (last = tail) != e) {
        //获取当前节点、以及前驱节点和后继节点
        LinkedHashMap.Entry < K, V > p = (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after;

        //将当前节点的后继节点指针指向空,使其和后继节点断开联系
        p.after = null;
        //如果前驱节点为空,则说明当前节点是链表的首节点,故将后继节点设置为首节点
        if (b == null) {
            head = a;
        } else {
            //如果后继节点不为空,则让前驱节点指向后继节点
            b.after = a;
        }
        //如果后继节点不为空,则让后继节点指向前驱节点
        if (a != null) {
            a.before = b;
        } else {
            //如果后继节点为空,则说明当前节点在链表最末尾,直接让last 指向前驱节点,这个 else其实 没有意义,因为最开头if已经确保了p不是尾结点了,自然after不会是null
            last = b;
        }
        //如果last为空,则说明当前链表只有一个节点p,则将head指向p
        if (last == null) {
            head = p;
        } else {
            //反之让p的前驱指针指向尾节点,再让尾节点的前驱指针指向p
            p.before = last;
            last.after = p;
        }
        //tail指向p,自此将节点p移动到链表末尾
        tail = p;
        ++modCount;
    }
}

remove 方法?

LinkedHashMap 并没有重写 remove 方法,而是实现了 HashMap 的空方法 afterNodeRemoval 来维护双向链表的指针; remove 的具体实现参考 HashMap.remove();

afterNodeRemoval 方法详解?

afterNodeRemoval 用来维护节点删除后的双向链表;

  1. 获取当前节点、前驱、后继节点;
  2. 将当前节点的前驱指针、后继指针置空;
  3. 判断前驱节点是否为 null,为 null 表示当前节点是头节点,将后继节点赋值给 head,否则将前驱节点的 next 指针指向后继节点;
  4. 判断后继节点是否为 null,如果为 null 表示当前节点是尾节点,把前驱节点赋值给 tail,否则后继节点的 before 指针指向前驱节点;
java 复制代码
void afterNodeRemoval(Node<K,V> e) {
    //获取当前节点 p、以及 e 的前驱节点 b 和后继节点 a;
    LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    //将 p 的前驱和后继指针都设置为 null,使其和前驱、后继节点断开联系;
    p.before = p.after = null;
    //如果前驱节点为空,则说明当前节点 p 是链表首节点,让 head 指针指向后继节点 a 即可;
    if (b == null)
        head = a;
    else
    //如果前驱节点 b 不为空,则让 b 直接指向后继节点 a;
        b.after = a;
    //如果后继节点为空,则说明当前节点 p 在链表末端,所以直接让 tail 指针指向前驱节点 b 即可;
    if (a == null)
        tail = b;
    else
    //反之后继节点的前驱指针直接指向前驱节点;
        a.before = b;
}

put 方法?

put 方法也没有被重写,但是为了维护双向链表的有序性做了两件事:

  • 重写 afterNodeAccess 方法,由于 LinkedHashMap 插入的节点会被移动到链表末端,对于已经存在的 key 被 put 后,需要通过 afterNodeAccess 将节点移动到链表末端;
  • 重写 afterNodeInsertion 方法,当 removeEldestEntry 返回 true 时,会将链表首节点移除;
java 复制代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
          //略
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                 //如果当前的key在map中存在,则调用 afterNodeAccess
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
         //调用插入后置方法,该方法被 LinkedHashMap 重写
        afterNodeInsertion(evict);
        return null;
    }

afterNodeInsertion 方法?

java 复制代码
void afterNodeInsertion(boolean evict) {
    LinkedHashMap.Entry<K,V> first;
    //如果evict为true且队首元素不为空以及removeEldestEntry返回true,则说明我们需要最老的元素(即在链表首部的元素)移除。
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        //获取链表首部的键值对的key
        K key = first.key;
        //调用removeNode将元素从HashMap的bucket中移除,并和LinkedHashMap的双向链表断开
        removeNode(hash(key), key, null, false, true);
    }
}

LinkedHashMap 和 HashMap 遍历性能比较?

  1. LinkedHashMap 维护了一个双向链表来记录数据插入的顺序,因此在迭代遍历生成的迭代器的时候,是按照双向链表的路径进行遍历的。
  2. HashMap 是按照桶迭代,遍历时,根据桶找到下一个不为空的元素;
java 复制代码
 final class EntryIterator extends HashIterator implements Iterator < Map.Entry < K, V >> {
     public final Map.Entry < K,V > next() {
         return nextNode();
     }
 }

 //获取下一个Node
 final Node < K, V > nextNode() {
     Node < K, V > [] t;
     //获取下一个元素next
     Node < K, V > e = next;
     if (modCount != expectedModCount)
         throw new ConcurrentModificationException();
     if (e == null)
         throw new NoSuchElementException();
     //将next指向bucket中下一个不为空的Node
     if ((next = (current = e).next) == null && (t = table) != null) {
         do {} while (index < t.length && (next = t[index++]) == null);
     }
     return e;
 }
java 复制代码
 final class LinkedEntryIterator extends LinkedHashIterator implements Iterator < Map.Entry < K, V >> {
     public final Map.Entry < K, V > next() {
         return nextNode();
     }
 }
 //获取下一个Node
 final LinkedHashMap.Entry < K, V > nextNode() {
     //获取下一个节点next
     LinkedHashMap.Entry < K, V > e = next;
     if (modCount != expectedModCount)
         throw new ConcurrentModificationException();
     if (e == null)
         throw new NoSuchElementException();
     //current 指针指向当前节点
     current = e;
     //next直接当前节点的after指针快速定位到下一个节点
     next = e.after;
     return e;
 }

什么是 LinkedHashMap?

LinkedHashMap 是 Java 集合框架中 HashMap 的一个子类,它继承了 HashMap 的所有属性和方法,并且在 HashMap 的基础重写了 afterNodeRemoval、afterNodeInsertion、afterNodeAccess 方法。使之拥有顺序插入和访问有序的特性。

LinkedHashMap 如何按照插入顺序迭代元素?

LinkedHashMap 按照插入顺序迭代元素是它的默认行为。LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序。因此,当使用迭代器迭代元素时,元素的顺序与它们最初插入的顺序相同。

LinkedHashMap 如何按照访问顺序迭代元素?

LinkedHashMap 可以通过构造函数中的 accessOrder 参数指定按照访问顺序迭代元素。当 accessOrder 为 true 时,每次访问一个元素时,该元素会被移动到链表的末尾,因此下次访问该元素时,它就会成为链表中的最后一个元素,从而实现按照访问顺序迭代元素。

LinkedHashMap 和 HashMap 有什么区别?

LinkedHashMap 和 HashMap 都是 Java 集合框架中的 Map 接口的实现类。它们的最大区别在于迭代元素的顺序。HashMap 迭代元素的顺序是不确定的,而 LinkedHashMap 提供了按照插入顺序或访问顺序迭代元素的功能。此外,LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序或访问顺序,而 HashMap 则没有这个链表。因此,LinkedHashMap 的插入性能可能会比 HashMap 略低,但它提供了更多的功能并且迭代效率相较于 HashMap 更加高效。

CopyOnWriteArrayList 源码分析

CopyOnWriteArrayList 的优势?

对于读取操作大于写操作的场景来说,由于读取数据不会修改数据,所以读取时加锁是一种资源浪费,因此读取的时候我们应该允许多个线程同时读取。

支持这种思路的有可重入读写锁(ReentrantReadWriteLock),即读读不互斥,读写互斥,写写互斥。但是 CopyOnWriteArrayList 更进一步,把读的性能提高到极致。CopyOnWriteArrayList 中,读读不互斥、读写不互斥、写写互斥,这样读的性能可以大幅提升。

CopyOnWriteArrayList 采用了写时复制策略。其实现逻辑是,在写或者修改时,把底层数组进行复制然后修改,修改完成后用新数组替换旧数组,这样就避免了读写互斥。

写时复制的缺点?

  • 内存占用:每次写操作都需要复制一份原始数据,会占用额外的内存空间,在数据量比较大的情况下,可能会导致内存资源不足。
  • 写操作开销:每一次写操作都需要复制一份原始数据,然后再进行修改和替换,所以写操作的开销相对较大,在写入比较频繁的场景下,性能可能会受到影响。
  • 数据一致性问题:修改操作不会立即反映到最终结果中,还需要等待复制完成,这可能会导致一定的数据一致性问题。

CopyOnWriteArrayList 类定义?

  • List: 它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问;
  • RandomAccess: 这是一个标志接口,表明实现这个接口的 List 集合是支持快速随机访问;
  • Cloneable: 表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作; -Serializable: 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输;
java 复制代码
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    //...
}

CopyOnWriteArrayList 构造函数?

  • CopyOnWriteArrayList 中有一个无参构造函数和两个有参构造函数。
java 复制代码
// 创建一个空的 CopyOnWriteArrayList
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

// 按照集合的迭代器返回的顺序创建一个包含指定集合元素的 CopyOnWriteArrayList
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}

// 创建一个包含指定数组的副本的列表
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

CopyOnWriteArrayList 插入元素?

CopyOnWriteArrayList 支持三种方式插入元素:

  • add(E e):尾部插入元素;
  • add(int index, E element):在指定位置插入元素;
  • addIfAbsent(E e):如果指定元素不存在,那么添加该元素。如果成功添加元素则返回 true;

添加元素源码?

java 复制代码
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 获取原来的数组
        Object[] elements = getArray();
        // 原来数组的长度
        int len = elements.length;
        // 创建一个长度+1的新数组,并将原来数组的元素复制给新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 元素放在新数组末尾
        newElements[len] = e;
        // array指向新数组
        setArray(newElements);
        return true;
    } finally {
        // 解锁
        lock.unlock();
    }
}
  • ReentrantLock 加锁避免多个线程写复制多份副本;
  • 写时复制,写的时候把底层数组进行了复制,写完后指向新的数组;
  • 由于每次写都要复制,所以时间复杂度为 O(n);
  • 没有类似 ArrayList 的扩容机制;

读取元素?

通过 index 进行随机访问,访问的过程是,首先获得当前数组的引用,然后用 index 获取对应下标的元素。由于 get 方法是弱一致性的,在某些情况下,可能读到旧的元素值。

获取列表中元素的个数?

java 复制代码
public int size() {
    return getArray().length;
}

CopyOnWriteArrayList中的array数组每次复制都刚好能够容纳下所有元素,并不像ArrayList那样会预留一定的空间。因此,CopyOnWriteArrayList中并没有size属性CopyOnWriteArrayList的底层数组的长度就是元素个数,因此size()方法只要返回数组长度就可以了。

删除元素?

java 复制代码
public E remove(int index) {
    // 获取可重入锁
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
         //获取当前array数组
        Object[] elements = getArray();
        // 获取当前array长度
        int len = elements.length;
        //获取指定索引的元素(旧值)
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        // 判断删除的是否是最后一个元素
        if (numMoved == 0)
             // 如果删除的是最后一个元素,直接复制该元素前的所有元素到新的数组
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            // 分段复制,将index前的元素和index+1后的元素复制到新数组
            // 新数组长度为旧数组长度-1
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            //将新数组赋值给array引用
            setArray(newElements);
        }
        return oldValue;
    } finally {
         // 解锁
        lock.unlock();
    }
}

判断元素是否存在?

java 复制代码
// 判断是否包含指定元素
public boolean contains(Object o) {
    //获取当前array数组
    Object[] elements = getArray();
    //调用index尝试查找指定元素,如果返回值大于等于0,则返回true,否则返回false
    return indexOf(o, elements, 0, elements.length) >= 0;
}

// 判断是否保证指定集合的全部元素
public boolean containsAll(Collection<?> c) {
    //获取当前array数组
    Object[] elements = getArray();
    //获取数组长度
    int len = elements.length;
    //遍历指定集合
    for (Object e : c) {
        //循环调用indexOf方法判断,只要有一个没有包含就直接返回false
        if (indexOf(e, elements, 0, len) < 0)
            return false;
    }
    //最后表示全部包含或者制定集合为空集合,那么返回true
    return true;
}

ArrayBlockingQueue 源码分析

ArrayBlockingQueue 简介?

  • java 1.5 增加了 JUC 包,这个包里包含了各种并发控制工具、并发容器、原子类等;
  • 为了解决高并发场景下多线程之间数据共享的问题,java 1.5 提供了 ArrayBlockingQueue 和 LinkedBlockingQueue, 它们是具有生产者-消费者模式实现的并发容器;
  • ArrayBlockingQueue 是有界队列,添加的容器达到上限之后,再次添加就会被阻塞或者抛出异常;
  • LinkedBlockingQueue 是由链表构造的队列,因此 LinkedBlockingQueue 可以设置队列是否有界。这里的有界是指队列的元素可以达到 Integer.MAX_VALUE,近乎无限大;

阻塞队列的思想?

  1. 当阻塞队列数据为空时,所有的消费者线程都会被阻塞,等待队列非空;
  2. 当生产者往队列里填充数据后,队列就会通知消费者队列非空,消费者此时就可以进来消费;
  3. 当阻塞队列因为消费者消费过慢或者生产者生产过快导致队列填满时,无法容纳新元素时,生产者就会被阻塞,等等队列非满时继续存放元素;
  4. 当消费者从队列中消费一个元素之后,队列就会通知生产者队列非满,生产者可以继续填充数据;

阻塞队列最常见的例子是线程池,在我们定义线程池时,需要指定一个阻塞队列。

ArrayBlockingQueue 构造方法?

ArrayBlockingQueue 一共有三个构造方法,下面是核心构造方法,其他构造方法都是基于这个构造方法实现的。

java 复制代码
// capacity 表示队列初始容量,fair 表示锁的公平性
public ArrayBlockingQueue(int capacity, boolean fair) {
  //如果设置的队列大小小于0,则直接抛出IllegalArgumentException
  if (capacity <= 0)
      throw new IllegalArgumentException();
  //初始化一个数组用于存放队列的元素
  this.items = new Object[capacity];
  //创建阻塞队列流程控制的锁
  lock = new ReentrantLock(fair);
  //用lock锁创建两个条件控制队列生产和消费
  notEmpty = lock.newCondition();
  notFull =  lock.newCondition();
}

剩下两个构造函数,一个是指定容量,默认非公平锁,还有一个是可以指定容量和锁的公平性,以及一个集合。

阻塞式获取和新增元素?

  • put(E e):将元素插入队列中,如果队列已满,则该方法会一直阻塞,直到队列有空间可用或者线程被中断;
  • take():获取并移除队列头部的元素,如果队列为空,则该方法会一直阻塞,直到队列非空或者线程被中断; 实现阻式新增和获取元素的关键在于非空(noEmpty)和非满(NotFull)两个条件对象。

阻塞添加元素 put 方法源码分析?

java 复制代码
public void put(E e) throws InterruptedException {
    //确保插入的元素不为null
    checkNotNull(e);
    //加锁
    final ReentrantLock lock = this.lock;
    //这里使用lockInterruptibly()方法而不是lock()方法是为了能够响应中断操作,如果在等待获取锁的过程中被打断则该方法会抛出InterruptedException异常。
    lock.lockInterruptibly();
    try {
        //如果count等数组长度则说明队列已满,当前线程将被挂起放到AQS队列中,等待队列非满时插入(非满条件)。
        //在等待期间,锁会被释放,其他线程可以继续对队列进行操作。
        while (count == items.length)
            notFull.await();
        //如果队列可以存放元素,则调用enqueue将元素入队
        enqueue(e);
    } finally {
        //释放锁
        lock.unlock();
    }
}

private void enqueue(E x) {
   //获取队列底层的数组
    final Object[] items = this.items;
    //将put index位置的值设置为我们传入的x
    items[putIndex] = x;
    //更新put index,如果put index等于数组长度,则更新为0
    if (++putIndex == items.length)
        putIndex = 0;
    //队列长度+1
    count++;
    //通知队列非空,那些因为获取元素而阻塞的线程可以继续工作了
    notEmpty.signal();
}

阻塞获取元素 take 方法源码分析?

java 复制代码
public E take() throws InterruptedException {
       //获取锁
     final ReentrantLock lock = this.lock;
     lock.lockInterruptibly();
     try {
        //如果队列中元素个数为0,则将当前线程打断并存入AQS队列中,等待队列非空时获取并移除元素(非空条件)
         while (count == 0)
             notEmpty.await();
        //如果队列不为空则调用dequeue获取元素
         return dequeue();
     } finally {
         //释放锁
         lock.unlock();
     }
}
java 复制代码
private E dequeue() {
  //获取阻塞队列底层的数组
  final Object[] items = this.items;
  @SuppressWarnings("unchecked")
  //从队列中获取takeIndex位置的元素
  E x = (E) items[takeIndex];
  //将takeIndex置空
  items[takeIndex] = null;
  //take Index向后挪动,如果等于数组长度则更新为 0,(因为 index 比 length 小 1, 所以自增后比较)
  if (++takeIndex == items.length)
      takeIndex = 0;
  //队列长度减1
  count--;
  if (itrs != null)
      itrs.elementDequeued();
  //通知那些被打断的线程当前队列状态非满,可以继续存放元素
  notFull.signal();
  return x;
}

非阻塞式获取和新增元素?

当插入或者获取失败时,返回特殊值:

  • offer(E e):将元素插入队列尾部。如果队列已满,则该方法会直接返回 false,不会等待并阻塞线程。实现方式跟阻塞类似,只是在调用 enqueue 之前,判断队列是否已满,满了直接返回 false。
  • poll():获取并移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。实现方式跟阻塞类似,只是在调用 dequeue 之前,判断队列是否为空,如果为空,直接返回 null。

offer 和 poll 来自 Queue 接口。

当插入或者获取失败时,抛出异常:

  • add(E e):将元素插入队列尾部。如果队列已满则会抛出 IllegalStateException 异常,底层基于 offer(E e) 方法,如果 offer 返回 false 抛出异常,否则返回 true。
  • remove():移除队列头部的元素,如果队列为空则会抛出 NoSuchElementException 异常,底层基于 poll(),如果 pull 返回 null 抛出异常,否则返回值。

add 和 remove 来自 Collection 接口。

指定超时时间内阻塞式获取和新增元素?

offer 和 poll 在非阻塞获取和新增元素的基础上,提供了带有等待时间的用于在指定的超时时间内阻塞式获取和添加元素。

java 复制代码
 public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
        checkNotNull(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        //队列已满,进入循环
            while (count == items.length) {
            //时间到了队列还是满的,则直接返回false
                if (nanos <= 0)
                    return false;
                 //阻塞nanos时间,等待非满
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }
java 复制代码
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
          //队列为空,循环等待,若时间到还是空的,则直接返回null
            while (count == 0) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

ArrayBlockingQueue 判断元素是否存在?

java 复制代码
public boolean contains(Object o) {
    //若目标元素为空,则直接返回 false
    if (o == null) return false;
    //获取当前队列的元素数组
    final Object[] items = this.items;
    //加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 如果队列非空
        if (count > 0) {
            final int putIndex = this.putIndex;
            //从队列头部开始遍历
            int i = takeIndex;
            do {
                if (o.equals(items[i]))
                    return true;
                if (++i == items.length)
                    i = 0;
            } while (i != putIndex);
        }
        return false;
    } finally {
        //释放锁
        lock.unlock();
    }
}

ArrayBlockingQueue 是什么?它的特点是什么?

  • ArrayBlockingQueue 是 BlockingQueue 接口的有界队列实现类,常用于多线程之间的数据共享,底层采用数组实现,从其名字就能看出来了。
  • ArrayBlockingQueue 的容量有限,一旦创建,容量不能改变。
  • 为了保证线程安全,ArrayBlockingQueue 的并发控制采用可重入锁 ReentrantLock ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。并且,它还支持公平和非公平两种方式的锁访问机制,默认是非公平锁。
  • ArrayBlockingQueue 虽名为阻塞队列,但也支持非阻塞获取和新增元素(例如 poll() 和 offer(E e) 方法),只是队列满时添加元素会抛出异常,队列为空时获取的元素为 null,一般不会使用。

ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?

ArrayBlockingQueue 和 LinkedBlockingQueue 是 Java 并发包中常用的两种阻塞队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别:

  • 底层实现:ArrayBlockingQueue 基于数组实现,而 LinkedBlockingQueue 基于链表实现。
  • 是否有界:ArrayBlockingQueue 是有界队列,必须在创建时指定容量大小。LinkedBlockingQueue 创建时可以不指定容量大小,默认是Integer.MAX_VALUE,也就是无界的。但也可以指定队列大小,从而成为有界的。
  • 锁是否分离: ArrayBlockingQueue中的锁是没有分离的,即生产和消费用的是同一个锁;LinkedBlockingQueue中的锁是分离的,即生产用的是putLock,消费是takeLock,这样可以防止生产者和消费者线程之间的锁争夺。
  • 内存占用:ArrayBlockingQueue 需要提前分配数组内存,而 LinkedBlockingQueue 则是动态分配链表节点内存。这意味着,ArrayBlockingQueue 在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而LinkedBlockingQueue 则是根据元素的增加而逐渐占用内存空间。

ArrayBlockingQueue 和 ConcurrentLinkedQueue 有什么区别?

ArrayBlockingQueue 和 ConcurrentLinkedQueue 是 Java 并发包中常用的两种队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别:

  • 底层实现:ArrayBlockingQueue 基于数组实现,而 ConcurrentLinkedQueue 基于链表实现。
  • 是否有界:ArrayBlockingQueue 是有界队列,必须在创建时指定容量大小,而 ConcurrentLinkedQueue 是无界队列,可以动态地增加容量。
  • 是否阻塞:ArrayBlockingQueue 支持阻塞和非阻塞两种获取和新增元素的方式(一般只会使用前者), ConcurrentLinkedQueue 是无界的,仅支持非阻塞式获取和新增元素。

ArrayBlockingQueue 的实现原理是什么?

  • ArrayBlockingQueue 内部维护一个定长的数组用于存储元素。
  • 通过使用 ReentrantLock 锁对象对读写操作进行同步,即通过锁机制来实现线程安全。
  • 通过 Condition 实现线程间的等待和唤醒操作。
  • 当队列已满时,生产者线程会调用 notFull.await() 方法让生产者进行等待,等待队列非满时插入(非满条件)。
  • 当队列为空时,消费者线程会调用 notEmpty.await()方法让消费者进行等待,等待队列非空时消费(非空条件)。
  • 当有新的元素被添加时,生产者线程会调用 notEmpty.signal()方法唤醒正在等待消费的消费者线程。
  • 当队列中有元素被取出时,消费者线程会调用 notFull.signal()方法唤醒正在等待插入元素的生产者线程。

PriorityQueue 源码分析

PriorityQueue 简介?

  • PriorityQueue 是一个优先队列,它每次出队的都是优先级最高的元素。元素的优先级是由指定的 Comparator 决定的。
  • 它是基于数组和最小堆二叉堆来实现的,即父节点的键值总是小于或等于任何一个子节点的键值。
  • 基于数组实现的二叉堆有以下特点:对于数组中任意位置 n 上的元素,它的左孩子在 [2n + 1] 位置上,右孩子在 [2(n + 1)] 位置,它的父亲则在 [(n -1)/2] 上,根的位置在[0]。

PriorityQueue 构造函数?

  • PriorityQueue 默认的初始化大小是 11,包含一个 Object 数组用来存放元素和构建二叉堆,还有一个 comparator 比较器。
  • PriorityQueue 构造函数会初始化 Object 数组和 comparator 比较器。
  • PriorityQueue 还支持从指定的 Collection/PriorityQueue/SortedSet 构造 PriorityQueue。

PriorityQueue 入队原理?

根据二叉堆(最小堆为例)的特点:

  • 父节点的键值总是小于或等于任何一个子节点的键值;
  • 基于数组实现的二叉堆,对于数组中任意位置 n 上的元素,它的左孩子在[2n + 1]位置上,右孩子在 [2(n + 1)]位置上,它的父亲在[(n - 1)/2],根节点在 [0]上。 因此,我们在添加元素时,按照如下步骤即可:
  1. 将新元素放在数组的最后一个位置;
  2. 根据它所在的位置找到它的父节点;
  3. 比较它根父节点的大小,如果小于父节点,则跟父节点进行交换,直到它大于等于它的父节点为止;

PriorityQueue 入队方法 add 源码分析?

java 复制代码
// 添加一个元素
public boolean add(E e) {
    return offer(e);
}

// 入队
public boolean offer(E e) {
    // 如果元素e为空,则排除空指针异常
    if (e == null)
        throw new NullPointerException();
    // 修改版本+1
    modCount++;
    // 记录当前队列中元素的个数
    int i = size ;
    // 如果当前元素个数大于等于队列底层数组的长度,则进行扩容
    if (i >= queue .length)
        grow(i + 1);
    // 元素个数+1
    size = i + 1;
    // 如果队列中没有元素,则将元素e直接添加至根(数组小标0的位置)
    if (i == 0)
        queue[0] = e;
    // 否则调用siftUp方法,将元素添加到尾部,进行上移判断
    else
        siftUp(i, e);
    return true;
}

数组扩容:

java 复制代码
private void grow(int minCapacity) {
    // 如果最小需要的容量大小minCapacity小于0,则说明此时已经超出int的范围,则抛出OutOfMemoryError异常
    if (minCapacity < 0) 
        throw new OutOfMemoryError();
    // 记录当前队列的长度
    int oldCapacity = queue .length;
    // 如果当前队列长度小于64则扩容2倍,否则扩容1.5倍
    int newCapacity = ((oldCapacity < 64)? ((oldCapacity + 1) * 2) :
 ((oldCapacity / 2) * 3));
    // 如果扩容后newCapacity超出int的范围,则将newCapacity赋值为Integer.Max_VALUE
    if (newCapacity < 0)
        newCapacity = Integer. MAX_VALUE;
    // 如果扩容后,newCapacity 小于最小需要的容量大小 minCapacity,则按找minCapacity 长度进行扩容
    if (newCapacity < minCapacity)
        newCapacity = minCapacity;
    // 数组copy,进行扩容
    queue = Arrays.copyOf( queue, newCapacity);
}
java 复制代码
// 上移,x表示新插入元素,k表示新插入元素在数组的位置
private void siftUp(int k, E x) {
    // 如果比较器comparator不为空,则调用siftUpUsingComparator方法进行上移操作
    if (comparator != null)
        siftUpUsingComparator(k, x);
    // 如果比较器comparator为空,则调用siftUpComparable方法进行上移操作
    else
        siftUpComparable(k, x);
}

private void siftUpComparable(int k, E x) {
    // 比较器comparator为空,需要插入的元素实现Comparable接口,用于比较大小
    Comparable<? super E> key = (Comparable<? super E>) x;
    // k>0表示判断k不是根的情况下,也就是元素x有父节点
    while (k > 0) {
        // 计算元素x的父节点位置[(n-1)/2]
        int parent = (k - 1) >>> 1;
        // 取出x的父亲e
        Object e = queue[parent];
        // 如果新增的元素k比其父亲e大,则不需要"上移",跳出循环结束
        if (key.compareTo((E) e) >= 0)
            break;
        // x比父亲小,则需要进行"上移"
        // 交换元素x和父亲e的位置
        queue[k] = e;
        // 将新插入元素的位置k指向父亲的位置,进行下一层循环
        k = parent;
    }
    // 找到新增元素x的合适位置k之后进行赋值
    queue[k] = key;
}

// 这个方法和上面的操作一样,不多说了
private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator .compare(x, (E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

二叉堆的删除根原理及 PriorityQueue 的出队实现?

  1. 首先将队尾元素删除;
  2. 比较根节点的最小的孩子与队尾节点的值,如果队尾节点的值大于根节点最小孩子的值,则需要把跟节点与它的最小孩子进行位置交换;
  3. 交换后,继续执行步骤 2,直到队尾节点小于根节点所在位置的最小子节点;
  4. 用队尾节点的值覆盖根节点的值,并返回根节点值,完成出队;
java 复制代码
// 删除并返回队头的元素,如果队列为空则抛出NoSuchElementException异常
public E remove() {
    E x = poll();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

// 删除并返回队头的元素,如果队列为空则返回null
public E poll() {
    // 队列为空,返回null
    if (size == 0)
        return null;
    // 队列元素个数-1
    int s = --size ;
    // 修改版本+1
    modCount++;
    // 队头的元素
    E result = (E) queue[0];
    // 队尾的元素
    E x = (E) queue[s];
    // 先将队尾赋值为null
    queue[s] = null;
    // 如果队列中不止队尾一个元素,则调用siftDown方法进行"下移"操作
    if (s != 0)
        siftDown(0, x);
    return result;
}

// 下移,x表示队尾的元素,k表示被删除元素在数组的位置
private void siftDown(int k, E x) {
    // 如果比较器comparator不为空,则调用siftDownUsingComparator方法进行下移操作
    if (comparator != null)
        siftDownUsingComparator(k, x);
    // 比较器comparator为空,则调用siftDownComparable方法进行下移操作
    else
        siftDownComparable(k, x);
}

private void siftDownComparable(int k, E x) {
    // 比较器comparator为空,需要插入的元素实现Comparable接口,用于比较大小
    Comparable<? super E> key = (Comparable<? super E>)x;
    // 通过size/2找到一个没有叶子节点的元素
    int half = size >>> 1; 
    // 比较位置k和half,如果k小于half,则k位置的元素就不是叶子节点
    while (k < half) {
        // 找到根元素的左孩子的位置[2n+1]
        int child = (k << 1) + 1;
        // 左孩子的元素
        Object c = queue[child];
        // 找到根元素的右孩子的位置[2(n+1)]
        int right = child + 1;
        // 如果左孩子大于右孩子,则将c复制为右孩子的值,这里也就是找出左右孩子哪个最小
        if (right < size &&
            ((Comparable<? super E>) c).compareTo((E) queue [right]) > 0)
            c = queue[child = right];
        // 如果队尾元素比根元素孩子都要小,则不需"下移",结束
        if (key.compareTo((E) c) <= 0)
            break;
        // 队尾元素比根元素孩子都大,则需要"下移"
        // 交换跟元素和孩子c的位置
        queue[k] = c;
        // 将根元素位置k指向最小孩子的位置,进入下层循环
        k = child;
    }
    // 找到队尾元素x的合适位置k之后进行赋值
    queue[k] = key;
}

// 这个方法和上面的操作一样,不多说了
private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            comparator.compare((E) c, (E) queue [right]) > 0)
            c = queue[child = right];
        if (comparator .compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

DelayQueue 源码分析

DelayQueue 简介?

DelayQueue 是 juc 包提供的延迟队列,用于实现延时任务,他是 BlockingQueue 的一种,底层是基于 PriorityQueue 实现的一个无界队列,是线程安全的。DelayQueue 中存放的元素必须实现 Delayed 接口,并重写 getDelay() 方法,用来计算是否到期。DelayQueue 会按照过期时间升序排列元素。

DelayQueue 的核心成员变量?

java 复制代码
//可重入锁,实现线程安全的关键
private final transient ReentrantLock lock = new ReentrantLock();
//延迟队列底层存储数据的集合,确保元素按照到期时间升序排列
private final PriorityQueue<E> q = new PriorityQueue<E>();

//指向准备执行优先级最高的线程
private Thread leader = null;
//实现多线程之间等待唤醒的交互
private final Condition available = lock.newCondition();
  1. ReentrantLock 可重入锁控制线程安全。
  2. PriorityQueue 优先级队列实现排序,每次都直接从对首拿到优先级最高的元素。
  3. leader 用来实现领导者-追随者模式,只允许只有一个线程进行限时等待(带有时间的 await)获最早到期的取延迟元素,其他线程全部永久 await,这是为了防止大量线程进行限时等等,然后到时间自动被唤醒造进行锁竞争,导致资源浪费。
  4. Condition 条件等待和条件通知。

添加元素源码分析?

DelayQueue 添加元素的方法无论是 add、put、offer,本质都是调用 offer。 主要逻辑如下:

  • 获取锁;
  • 将元素添加到 PriorityQueue 中;
  • 调用 peek 方法,查看队首元素是不是刚刚插入的元素,如果是,将 leader 置为 null,并 signal 其他线程来抢占这个元素;
  • 最后释放锁;
java 复制代码
public boolean offer(E e) {
    //尝试获取lock
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        //如果上锁成功,则调q的offer方法将元素存放到优先队列中
        q.offer(e);
        //调用peek方法看看当前队首元素是否就是本次入队的元素,如果是则说明当前这个元素是即将到期的任务(即优先级最高的元素)
        if (q.peek() == e) {
            //将leader设置为空,通知调用取元素方法而阻塞的线程来争抢这个任务
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        //上述步骤执行完成,释放lock
        lock.unlock();
    }
}

获取元素 take 源码分析?

  1. 获取锁;
  2. 获取队列的第一个元素,如果为 null,表示队列为空,进行无限期等待(await);
  3. 获取第一个元素的 Delay 时间,如果 Delay 小于等于 0 ,表示已经到期,直接返回;
  4. 首先释放方法中对第一个元素的引用,防止后续不能被 gc 回收;
  5. 判断 leader 是否为 null,如果 leader 不为 null,说明有线程正在限时等等,当前线程陷入无限期等待;
  6. 否则将 leader 设置为当前线程,进行限时等待,等待结束后,将 leader 置为 null;
  7. 最后,如果获取到元素,且 leader 也是 null,随机 signal 一个线程进行下一次抢占,当前线程释放锁并返回;
java 复制代码
public E take() throws InterruptedException {
    // 尝试获取可重入锁,将底层AQS的state设置为1,并设置为独占锁
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
            //查看队列第一个元素
            E first = q.peek();
            //若为空,则将当前线程放入ConditionObject的等待队列中,并将底层AQS的state设置为0,表示释放锁并进入无限期等待
            if (first == null)
                available.await();
            else {
                //若元素不为空,则查看当前元素多久到期
                long delay = first.getDelay(NANOSECONDS);
                //如果小于0则说明已到期直接返回出去
                if (delay <= 0)
                    return q.poll();
                //如果大于0则说明任务还没到期,首先需要释放对这个元素的引用
                first = null; // don't retain ref while waiting
                //判断leader是否为空,如果不为空,则说明正有线程作为leader并等待一个任务到期,则当前线程进入无限期等待
                if (leader != null)
                    available.await();
                else {
                    //反之将我们的线程成为leader
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        //并进入有限期等待
                        available.awaitNanos(delay);
                    } finally {
                        //等待任务到期时,释放leader引用,进入下一次循环将任务return出去
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        // 收尾逻辑:当leader为null,并且队列中有任务时,唤醒等待的获取元素的线程。
        if (leader == null && q.peek() != null)
            available.signal();
        //释放锁
        lock.unlock();
    }
}

无阻塞获取元素?

  • 首先获取锁;
  • 当元素没有到期,或者元素为空时,直接返回null;否则直接 poll 返回元素;
  • 释放锁;
java 复制代码
public E poll() {
    //尝试获取可重入锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        //查看队列第一个元素,判断元素是否为空
        E first = q.peek();

        //若元素为空,或者元素未到期,则直接返回空
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            return null;
        else
            //若元素不为空且到期了,直接调用poll返回出去
            return q.poll();
    } finally {
        //释放可重入锁lock
        lock.unlock();
    }
}

DelayQueue 的实现原理是什么?

DelayQueue 底层是使用优先队列 PriorityQueue 来存储元素,而 PriorityQueue 采用二叉小顶堆的思想确保值小的元素排在最前面,这就使得延迟任务优先级高的任务始终在最前面。同时 DelayQueue 为了保证线程安全还用到了可重入锁 ReentrantLock,确保单位时间内只有一个线程可以操作延迟队列。最后,为了实现多线程之间等待和唤醒的交互效率,DelayQueue 还用到了 Condition,通过 Condition 的 await 和 signal 方法完成多线程之间的等待唤醒。

DelayQueue 的实现是否线程安全?

DelayQueue 的实现是线程安全的,它通过 ReentrantLock 实现了互斥访问和 Condition 实现了线程间的等待和唤醒操作,可以保证多线程环境下的安全性和可靠性。

DelayQueue 的使用场景有哪些?

DelayQueue 通常用于实现定时任务调度和缓存过期删除等场景。在定时任务调度中,需要将需要执行的任务封装成延迟任务对象,并将其添加到 DelayQueue 中,DelayQueue 会自动按照剩余延迟时间进行升序排序(默认情况),以保证任务能够按照时间先后顺序执行。对于缓存过期这个场景而言,在数据被缓存到内存之后,我们可以将缓存的 key 封装成一个延迟的删除任务,并将其添加到 DelayQueue 中,当数据过期时,拿到这个任务的 key,将这个 key 从内存中移除。

DelayQueue 中 Delayed 接口的作用是什么?

Delayed 接口定义了元素的剩余延迟时间(getDelay)和元素之间的比较规则(该接口继承了 Comparable 接口)。若希望元素能够存放到 DelayQueue 中,就必须实现 Delayed 接口的 getDelay() 方法和 compareTo() 方法,否则 DelayQueue 无法得知当前任务剩余时长和任务优先级的比较。

DelayQueue 和 Timer/TimerTask 的区别是什么?

DelayQueue 和 Timer/TimerTask 都可以用于实现定时任务调度,但是它们的实现方式不同。DelayQueue 是基于优先级队列和堆排序算法实现的,可以实现多个任务按照时间先后顺序执行;而 Timer/TimerTask 是基于单线程实现的,只能按照任务的执行顺序依次执行,如果某个任务执行时间过长,会影响其他任务的执行。另外,DelayQueue 还支持动态添加和移除任务,而 Timer/TimerTask 只能在创建时指定任务。

相关推荐
他日若遂凌云志20 分钟前
深入剖析 Fantasy 框架的消息设计与序列化机制:协同架构下的高效转换与场景适配
后端
快手技术36 分钟前
快手Klear-Reasoner登顶8B模型榜首,GPPO算法双效强化稳定性与探索能力!
后端
二闹1 小时前
三个注解,到底该用哪一个?别再傻傻分不清了!
后端
用户49055816081251 小时前
当控制面更新一条 ACL 规则时,如何更新给数据面
后端
林太白1 小时前
Nuxt.js搭建一个官网如何简单
前端·javascript·后端
码事漫谈1 小时前
VS Code 终端完全指南
后端
该用户已不存在1 小时前
OpenJDK、Temurin、GraalVM...到底该装哪个?
java·后端
怀刃2 小时前
内存监控对应解决方案
后端
码事漫谈2 小时前
VS Code Copilot 内联聊天与提示词技巧指南
后端
Moonbit2 小时前
MoonBit Perals Vol.06: MoonBit 与 LLVM 共舞 (上):编译前端实现
后端·算法·编程语言