图解Java并发容器: CHM、ConcurrentLinkedQueue、七种阻塞队列的使用场景和原理

ConcurrentHashMap 的使用和原理

Map 是一个接口,它的实现方式有很多种,比如常见的 HashMap、LinkedHashMap,但是这些 Map 的实现并不是线程安全的,在多线程高并发的环境中会出现线程安全的问题。

鉴于 Map 是一个在高并发的应用环境中应用比较广泛的数据结构,Doug Lea 自 JDK 1.5 版本起在 Java 中引入了 ConcurrentHashMap

ConcurrentHashMap 的使用

对一个技术的掌握,从使用开始。我们现在来实现一个一个高并发计数器,例如记录网站访问量、接口调用次数等。

arduino 复制代码
@Service
public class Counter {
    private final ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

    public void increase(String key) {
        map.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
    }

    public int get(String key) {
        return map.getOrDefault(key, 0);
    }
}
  • compute :这是一个并发安全原子操作,我们使用 compute 方法实现对计数器的增加操作。

a.如果 key 不存在则新建一个值为 1 的计数器。

b.否则将其 value 递增 1。

  • 通过 get 方法可以获取指定 key 对应的计数器值。

这个例子比较简单,现在开始上强度。ConcurrentHashMap 还可以用来实现缓存管理器,例如存储经常使用的业务数据、系统配置等信息,从而避免频繁的数据库查询或网络请求。

以下是一个支持过期时间、自动刷新和并发控制的缓存管理器实现,包含详细注释和最佳实践:

scss 复制代码
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;

/**
 * 高性能并发缓存管理器
 * @param <K> 键类型
 * @param <V> 值类型
 */
publicclass ConcurrentCache<K, V> {

    privatefinal ConcurrentHashMap<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();
    privatefinal Function<K, V> loader;  // 缓存加载器
    privatefinal ScheduledExecutorService cleaner;  // 过期清理线程

    // 默认配置
    privatelong defaultTTL = 30_000;          // 默认30秒
    privatelong cleanupInterval = 5_000;      // 5秒清理一次
    privateint maxRetries = 3;                // 最大重试次数
    privateboolean refreshOnAccess = true;    // 访问时刷新TTL

    public ConcurrentCache(Function<K, V> loader) {
        this.loader = loader;
        this.cleaner = Executors.newSingleThreadScheduledExecutor();
        startCleanupTask();
    }

    /**
     * 获取缓存值(线程安全)
     */
    public V get(K key) {
        CacheEntry<V> entry = cache.get(key);

        // 无缓存或已过期时加载
        if (entry == null || entry.isExpired()) {
            return loadAndCache(key);
        }

        // 更新访问时间(可选)
        if (refreshOnAccess) {
            entry.touch();
        }
        return entry.value;
    }

    /**
     * 原子性的加载和缓存操作
     */
    private V loadAndCache(K key) {
        int retry = 0;
        while (retry++ < maxRetries) {
            try {
                // 使用compute保证原子性
                CacheEntry<V> newEntry = cache.compute(key, (k, oldEntry) -> {
                    // 检查其他线程是否已经加载
                    if (oldEntry != null && !oldEntry.isExpired()) {
                        return oldEntry;
                    }

                    V value = loader.apply(k);
                    returnnew CacheEntry<>(value, defaultTTL, TimeUnit.MILLISECONDS);
                });

                return newEntry.value;
            } catch (Exception ex) {
                if (retry >= maxRetries) {
                    thrownew CacheLoadException("加载缓存失败,key=" + key, ex);
                }
                // 指数退避重试
                sleepUninterruptibly((long) Math.pow(2, retry), TimeUnit.MILLISECONDS);
            }
        }
        thrownew CacheLoadException("超过最大重试次数,key=" + key);
    }

    /**
     * 主动放入缓存(支持自定义TTL)
     */
    public void put(K key, V value, long ttl, TimeUnit unit) {
        cache.put(key, new CacheEntry<>(value, ttl, unit));
    }

    /**
     * 启动定期清理任务(双重检查锁模式)
     */
    private void startCleanupTask() {
        if (cleaner.isShutdown()) return;

        cleaner.scheduleWithFixedDelay(() -> {
            cache.forEach((key, entry) -> {
                if (entry.isExpired()) {
                    cache.remove(key, entry); // 使用CAS删除
                }
            });
        }, cleanupInterval, cleanupInterval, TimeUnit.MILLISECONDS);
    }

    // 其他实用方法
    public void remove(K key) { cache.remove(key); }
    public void clear() { cache.clear(); }
    public long size() { return cache.mappingCount(); }

    // 配置方法(Builder模式风格)
    public ConcurrentCache<K, V> defaultTTL(long ttl, TimeUnit unit) {
        this.defaultTTL = unit.toMillis(ttl);
        returnthis;
    }

    public ConcurrentCache<K, V> cleanupInterval(long interval, TimeUnit unit) {
        this.cleanupInterval = unit.toMillis(interval);
        returnthis;
    }

    // 异常处理
    privatestaticclass CacheLoadException extends RuntimeException {
        CacheLoadException(String message, Throwable cause) {
            super(message, cause);
        }

        CacheLoadException(String message) {
            super(message);
        }
    }

    // 工具方法:不可中断的休眠
    private static void sleepUninterruptibly(long duration, TimeUnit unit) {
        try {
            Thread.sleep(unit.toMillis(duration));
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
        }
    }

    // 关闭时释放资源
    public void shutdown() {
        cleaner.shutdownNow();
        cache.clear();
    }

    // 缓存条目:包含值、过期时间和访问时间戳
    privatestaticclass CacheEntry<V> {
        final V value;
        finallong expireAt;  // 绝对过期时间(纳秒)
        final AtomicLong accessTime = new AtomicLong(); // 最后访问时间(纳秒)

        CacheEntry(V value, long ttl, TimeUnit unit) {
            this.value = value;
            this.expireAt = System.nanoTime() + unit.toNanos(ttl);
            touch();
        }
        // 刷新访问时间
        void touch() {
            accessTime.set(System.nanoTime());
        }
        // 判断是否已过期
        boolean isExpired() {
            return System.nanoTime() > expireAt;
        }
    }
}

实现原理

CHM 的源码有 6k 多行,包含的内容多,精巧,不容易理解;建议在查看源码的时候,可以首先把握整体结构脉络,对于一些精巧的优化,哈希技巧可以先了解目的就可以了,不用深究。

对整体把握比较清楚后,在逐步分析,可以比较快速的看懂。

JDK1.8 版本中的 CHM,和 JDK1.7 版本的差别非常大,在查看资料的时候要注意区分,1.7 中主要是使用 Segment 分段锁 来解决并发问题的。

JDK 1.7 版本 ConcurrentHashMap

在 JDK1.7 版本中,ConcurrentHashMap 的数据结构是由一个 Segment 数组和多个 HashEntry 组成。

而每一个 Segment 元素存储的是 HashEntry 数组+链表,并对应一个 ReentrantLock 锁,用于并发访问控制。

以 put 操作为例,来看一下 ConcurrentHashMap 的实现过程:

  • 首先计算 key 的哈希值。
  • 根据哈希值找到对应的 Segment。
  • 获取 Segment 对应的锁。
  • 如果还没有元素,就直接插入到 Segment 中。
  • 如果已经存在元素,就循环比较 key 是否相等。
  • 如果 key 已经存在,就根据要求更新 value。
  • 如果 key 不存在,就插入新的元素(链表或者红黑树)。

上述操作中,步骤 2 到 3 相当于对对应的 Segment 加了一个悲观锁,如果 Segment 数组只有一个 Segment 元素,效果与 Hashtable 类似。

如果存在多个 Segment,效果就相当于使用了分段锁机制,提高了并发访问性能。

JDK 1.8 ConcurrentHashMap

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

其主要区别就在 CHM 支持并发:

  • 使用 Unsafe 方法操作数组内部元素,保证可见性;(U.getObjectVolatile、U.compareAndSwapObject、U.putObjectVolatile)。
  • 在更新和移动节点的时候,直接锁住对应的哈希桶,锁粒度更小,且动态扩展。
  • 针对扩容慢操作进行优化。

a.首先扩容过程的中,节点首先移动到过度表 nextTable ,所有节点移动完毕时替换散列表 table。

b.移动时先将散列表定长等分,然后逆序依次领取任务扩容,设置 sizeCtl 标记正在扩容。

c.移动完成一个哈希桶或者遇到空桶时,将其标记为 ForwardingNode 节点,并指向 nextTable 。

d.后有其他线程在操作哈希表时,遇到 ForwardingNode 节点,则先帮助扩容(继续领取分段任务),扩容完成后再继续之前的操作。

  • 优化哈希表计数器,采用 LongAdder、Striped64 类似思想。
  • 以及大量的哈希算法优化和状态变量优化。

类定义和成员变量

ini 复制代码
// node数组最大容量:2^30=1073741824
privatestaticfinalint MAXIMUM_CAPACITY = 1 << 30;
// 默认初始值,必须是2的幕数
privatestaticfinalint DEFAULT_CAPACITY = 16;
//数组可能最大值,需要与toArray()相关方法关联
staticfinalint MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//并发级别,遗留下来的,为兼容以前的版本
privatestaticfinalint DEFAULT_CONCURRENCY_LEVEL = 16;
// 负载因子
privatestaticfinalfloat LOAD_FACTOR = 0.75f;
// 链表转红黑树阀值,> 8 链表转换为红黑树
staticfinalint TREEIFY_THRESHOLD = 8;
//树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
staticfinalint UNTREEIFY_THRESHOLD = 6;
staticfinalint MIN_TREEIFY_CAPACITY = 64;
privatestaticfinalint MIN_TRANSFER_STRIDE = 16;
privatestaticint RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大线程数
privatestaticfinalint MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中记录size大小的偏移量
privatestaticfinalint RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
staticfinalint MOVED     = -1;
// 树根节点的hash值
staticfinalint TREEBIN   = -2;
// ReservationNode的hash值
staticfinalint RESERVED  = -3;
// 可用处理器数量
staticfinalint NCPU = Runtime.getRuntime().availableProcessors();
//存放node的数组
transientvolatile Node<K,V>[] table;
/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
 *当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
 *当为0时:代表当时的table还没有被初始化
 *当为正数时:表示初始化或者下一次进行扩容的大小
 */
privatetransientvolatileint sizeCtl;

上面有几个重要的地方这里单独讲:

LOAD_FACTOR:

这里的负载系数,同 HashMap 等其他 Map 的系数有明显区别:

  • 通常的系数默认 0.75,可以由构造函数传入,当节点数 size 超过 loadFactor * capacity 时扩容。
  • 而 CMH 的系数则固定 0.75(使用 n - (n >>> 2) 表示),构造函数传入的系数只影响初始化容量,见第 5 个构造函数。

sizeCtl:

sizeCtl 是 CHM 中最重要的状态变量,其中包括很多中状态,这里先整体介绍帮助后面源码理解。

  • sizeCtl = 0 :初始值,还未指定初始容量。
  • sizeCtl > 0 :

a.table 未初始化,表示初始化容量。

b.table 已初始化,表示扩容阈值(0.75n)。

  • sizeCtl = -1 :表示正在初始化。
  • sizeCtl < -1 :表示正在扩容,具体结构如图所示:

Node 节点

Node 是 ConcurrentHashMap 存储结构的基本单元,继承于 HashMap 中的 Entry,用于存储数据,源代码如下。

typescript 复制代码
static class Node<K,V> implements Map.Entry<K,V> {  // 哈希表普通节点
finalint hash;
final K key;
volatile V val;
volatile Node<K,V> next;

Node<K,V> find(int h, Object k) {}   // 主要在扩容时,利用多态查询已转移节点
}

staticfinalclass ForwardingNode<K,V> extends Node<K,V> {  // 标识扩容节点
final Node<K,V>[] nextTable;  // 指向成员变量 ConcurrentHashMap.nextTable

  ForwardingNode(Node<K,V>[] tab) {
    super(MOVED, null, null, null);  // hash = -1,快速确定 ForwardingNode 节点
    this.nextTable = tab;
  }

Node<K,V> find(int h, Object k) {}
}

staticfinalclass TreeBin<K,V> extends Node<K,V> { // 红黑树根节点
  TreeBin(TreeNode<K,V> b) {
    super(TREEBIN, null, null, null);  // hash = -2,快速确定红黑树,
    ...
  }
}
staticfinalclass TreeNode<K,V> extends Node<K,V> { } // 红黑树普通节点,其 hash 同 Node 普通节点 > 0;

哈希计算

java 复制代码
static finalint MOVED     = -1;          // hash for forwarding nodes
staticfinalint TREEBIN   = -2;          // hash for roots of trees
staticfinalint RESERVED  = -3;          // hash for transient reservations
staticfinalint HASH_BITS = 0x7fffffff;  // usable bits of normal node hash

// 让高位16位,参与哈希桶定位运算的同时,保证 hash 为正
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}

哈希桶可见性

一个数组即使声明为 volatile,也只能保证这个数组引用本身的可见性,其内部元素的可见性是无法保证的,如果每次都加锁,则效率必然大大降低,在 CHM 中则使用 Unsafe 方法来保证:

arduino 复制代码
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

staticfinal <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

staticfinal <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
  U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

put 操作

思路是对当前的 table 进行无条件自循环直到 put 成功,可以分成以下六步流程来概述。

  • 如果没有初始化就先调用 initTable()方法来进行初始化过程。
  • 如果没有 hash 冲突就直接 CAS 插入。
  • 如果还在进行扩容操作就先进行扩容。
  • 如果存在 hash 冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入。
  • 最后一个如果该链表的数量大于阈值 8,就要先转换成黑红树的结构,break 再一次进入循环。
  • 如果添加成功就调用 addCount()方法统计 size,并且检查是否需要扩容。
ini 复制代码
public V put(K key, V value) {
    return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) thrownew NullPointerException();
    int hash = spread(key.hashCode()); //两次hash,减少hash冲突,可以均匀分布
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) { //对这个table进行迭代
        Node<K,V> f; int n, i, fh;
        //这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        elseif ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果i位置没有数据,就直接无锁插入
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        elseif ((fh = f.hash) == MOVED)//如果在进行扩容,则先进行扩容操作
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //如果以上条件都不满足,那就要进行加锁操作,也就是存在hash冲突,锁住链表或者红黑树的头结点
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) { //表示该节点是链表结构
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //这里涉及到相同的key进行put就会覆盖原先的value
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {  //插入链表尾部
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    elseif (f instanceof TreeBin) {//红黑树结构
                        Node<K,V> p;
                        binCount = 2;
                        //红黑树结构旋转插入
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) { //如果链表的长度大于8时就会进行红黑树的转换
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);//统计size,并且检查是否需要扩容
    returnnull;
}

流程图如下所示:

get 操作

get 方法可能看代码不是很长,但是他却能 保证无锁状态下的内存一致性 。

kotlin 复制代码
public V get(Object key) {
  Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode()); // 计算 hash
if ((tab = table) != null && (n = tab.length) > 0 &&  // 确保 table 已经初始化

    // 确保对应的哈希桶不为空,注意这里是 Volatile 语义获取;因为扩容的时候,是完全拷贝,所以只要不为空,则链表必然完整
    (e = tabAt(tab, (n - 1) & h)) != null) {
    if ((eh = e.hash) == h) {
      if ((ek = e.key) == key || (ek != null && key.equals(ek)))
        return e.val;
    }

    // hash < 0,则必然在扩容,原来位置的节点可能全部移动到 i + oldCap 位置,所以利用多态到 nextTable 中查找
    elseif (eh < 0) return (p = e.find(h, key)) != null ? p.val : null;

    while ((e = e.next) != null) { // 遍历链表
      if (e.hash == h &&
        ((ek = e.key) == key || (ek != null && key.equals(ek))))
        return e.val;
    }
  }
return null;
}

ConcurrentHashMap 的 get 操作的流程很简单,也很清晰,可以分为三个步骤来描述。

  • 计算 hash 值,定位到该 table 索引位置,如果是首节点符合就返回。
  • 如果遇到扩容的时候,会调用标志正在扩容节点 ForwardingNode 的 find 方法,查找该节点,匹配就返回。
  • 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回 null。

size 操作

在 JDK1.8 版本中,对于 size 的计算,在扩容和 addCount()方法就已经有处理了,JDK1.7 是在调用 size()方法才去计算,其实在并发集合中去计算 size 是没有多大的意义的,因为 size 是实时在变的,只能计算某一刻的大小,但是某一刻太快了,人的感知是一个时间段,所以并不是很精确。

扩容

扩容操作一直都是比较慢的操作,而 CHM 中巧妙的利用任务划分,使得多个线程可能同时参与扩容。

另外扩容条件也有两个:

  • 有链表长度超过 8,但是容量小于 64 的时候,发生扩容。
  • 节点数超过阈值的时候,发生扩容。

其扩容的过程可描述为:

  • 首先扩容过程的中,节点首先移动到过度表 nextTable ,所有节点移动完毕时替换散列表 table。
  • 移动时先将散列表定长等分,然后逆序依次领取任务扩容,设置 sizeCtl 标记正在扩容。
  • 移动完成一个哈希桶或者遇到空桶时,将其标记为 ForwardingNode 节点,并指向 nextTable 。
  • 后有其他线程在操作哈希表时,遇到 ForwardingNode 节点,则先帮助扩容(继续领取分段任务),扩容完成后再继续之前的操作。

如图:

具体源码如下。

ini 复制代码
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
    stride = MIN_TRANSFER_STRIDE; // 根据 CPU 数量计算任务步长
if (nextTab == null) {          // 初始化 nextTab
    try {
      @SuppressWarnings("unchecked")
      Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];  // 扩容一倍
      nextTab = nt;
    } catch (Throwable ex) {
      sizeCtl = Integer.MAX_VALUE; // 发生 OOM 时,不再扩容
      return;
    }
    nextTable = nextTab;
    transferIndex = n;
  }
int nextn = nextTab.length;
  ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);  // 标记空桶,或已经转移完毕的桶
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {  // 逆向遍历扩容
    Node<K,V> f; int fh;
    while (advance) {  // 向前获取哈希桶
      int nextIndex, nextBound;
      if (--i >= bound || finishing)               // 已经取到哈希桶,或已完成时退出
        advance = false;
      elseif ((nextIndex = transferIndex) <= 0) { // 遍历到达头节点,已经没有待迁移的桶,线程准备退出
        i = -1;
        advance = false;
      }
      elseif (U.compareAndSwapInt
           (this, TRANSFERINDEX, nextIndex,
            nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {  // 当前任务完成,领取下一批哈希桶
        bound = nextBound;
        i = nextIndex - 1;  // 索引指向下一批哈希桶
        advance = false;
      }
    }

    // i < 0  :表示扩容结束,已经没有待移动的哈希桶
    // i >= n :扩容结束,再次检查确认
    // i + n >= nextn : 在使用 nextTable 替换 table 时,有线程进入扩容就会出现
    if (i < 0 || i >= n || i + n >= nextn) { // 完成扩容准备退出
      int sc;
      if (finishing) {  // 两次检查,只有最后一个扩容线程退出时,才更新变量
        nextTable = null;
        table = nextTab;
        sizeCtl = (n << 1) - (n >>> 1); // 0.75*2*n
        return;
      }
      if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {  // 扩容线程减一
        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return;  // 不是最后一个线程,直接退出
        finishing = advance = true;   // 最后一个线程,再次检查
        i = n;                        // recheck before commit
      }
    }
    elseif ((f = tabAt(tab, i)) == null)  // 当前节点为空,直接标记为 ForwardingNode,然后继续获取下一个桶
      advance = casTabAt(tab, i, null, fwd);

    // 之前的线程已经完成该桶的移动,直接跳过,正常情况下自己的任务区间,不会出现 ForwardingNode 节点,
    elseif ((fh = f.hash) == MOVED)  // 此处为极端条件下的健壮性检查
      advance = true; // already processed

    // 开始处理链表
    else {
      // 注意在 get 的时候,可以无锁获取,是因为扩容是全拷贝节点,完成后最后在更新哈希桶
      // 而在 put 的时候,是直接将节点加入尾部,获取修改其中的值,此时如果允许 put 操作,最后就会发生脏读,
      // 所以 put 和 transfer,需要竞争同一把锁,也就是对应的哈希桶,以保证内存一致性效果
      synchronized (f) {
        if (tabAt(tab, i) == f) {  // 确认锁定的是同一个桶
          Node<K,V> ln, hn;
          if (fh >= 0) {  // 正常节点
            int runBit = fh & n;  // hash & n,判断扩容后的索引
            Node<K,V> lastRun = f;

            // 此处找到链表最后扩容后处于同一位置的连续节点,这样最后一节就不用再一次复制了
            for (Node<K,V> p = f.next; p != null; p = p.next) {
              int b = p.hash & n;
              if (b != runBit) {
                runBit = b;
                lastRun = p;
              }
            }
            if (runBit == 0) {
              ln = lastRun;
              hn = null;
            }
            else {
              hn = lastRun;
              ln = null;
            }

            // 依次将链表拆分成,lo、hi 两条链表,即位置不变的链表,和位置 + oldCap 的链表
            // 注意最后一节链表没有new,而是直接使用原来的节点
            // 同时链表的顺序也被打乱了,lastRun 到最后为正序,前面一节为逆序
            for (Node<K,V> p = f; p != lastRun; p = p.next) {
              int ph = p.hash; K pk = p.key; V pv = p.val;
              if ((ph & n) == 0)
                ln = new Node<K,V>(ph, pk, pv, ln);
              else
                hn = new Node<K,V>(ph, pk, pv, hn);
            }
            setTabAt(nextTab, i, ln);      // 插入 lo 链表
            setTabAt(nextTab, i + n, hn);  // 插入 hi 链表
            setTabAt(tab, i, fwd);         // 哈希桶移动完成,标记为 ForwardingNode 节点
            advance = true;                // 继续获取下一个桶
          }
          elseif (f instanceof TreeBin) { // 拆分红黑树
            TreeBin<K,V> t = (TreeBin<K,V>)f;
            TreeNode<K,V> lo = null, loTail = null; // 为避免最后在反向遍历,先留头结点的引用,
            TreeNode<K,V> hi = null, hiTail = null; // 因为顺序的链表,可以加速红黑树构造
            int lc = 0, hc = 0;  // 同样记录 lo,hi 链表的长度
            for (Node<K,V> e = t.first; e != null; e = e.next) {  // 中序遍历红黑树
              int h = e.hash;
              TreeNode<K,V> p = new TreeNode<K,V>(h, e.key, e.val, null, null);  // 构造红黑树节点
              if ((h & n) == 0) {
                if ((p.prev = loTail) == null)
                  lo = p;
                else
                  loTail.next = p;
                loTail = p;
                ++lc;
              }
              else {
                if ((p.prev = hiTail) == null)
                  hi = p;
                else
                  hiTail.next = p;
                hiTail = p;
                ++hc;
              }
            }

            // 判断是否需要将其转化为红黑树,同时如果只有一条链,那么就可以不用在构造
            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0) ? new TreeBin<K,V>(lo) : t;
            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0) ? new TreeBin<K,V>(hi) : t;
            setTabAt(nextTab, i, ln);
            setTabAt(nextTab, i + n, hn);
            setTabAt(tab, i, fwd);
            advance = true;
          }
        }
      }
    }
  }
}

ConcurrentLinkedQueue 的使用和原理

ConcurerntLinkedQueue 一个基于单向链表的无界线程安全队列,支持高并发的队列操作,无需显式的锁,而且容量没有上限。

此队列按照 FIFO(先进先出)原则对元素进行排序。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。

应用场景

常见的使用场景可能包括任务调度、事件处理、日志记录等。比如订单处理系统,其中多个生产者生成订单,多个消费者处理订单。

csharp 复制代码
// 订单事件处理器(生产环境级实现)
publicclass OrderEventProcessor {
    // 使用队列作为订单缓冲区(无容量限制)
    privatefinal ConcurrentLinkedQueue<OrderEvent> queue = new ConcurrentLinkedQueue<>();
    privatefinal ExecutorService workers = Executors.newFixedThreadPool(
        Runtime.getRuntime().availableProcessors() * 2,
        new NamedThreadFactory("order-processor")
    );

    // 初始化处理线程
    public void start() {
        for (int i = 0; i < workers.getCorePoolSize(); i++) {
            workers.submit(this::processEvents);
        }
    }

    // 接收订单事件(来自网络IO线程)
    public void receiveEvent(OrderEvent event) {
        queue.offer(event); // 无阻塞插入
        metrics.recordEnqueue(); // 监控埋点
    }

    // 事件处理核心逻辑
    private void processEvents() {
        while (!Thread.currentThread().isInterrupted()) {
            OrderEvent event = queue.poll(); // 无阻塞获取
            if (event != null) {
                try {
                    handleEvent(event);
                } catch (Exception ex) {
                    handleFailure(event, ex); // 异常处理
                }
            } else {
                // 队列空时自适应休眠(避免CPU空转)
                sleepBackoff();
            }
        }
    }

    // 指数退避休眠(动态调节CPU使用率)
    private void sleepBackoff() {
        long delay = 1; // 初始1ms
        while (queue.isEmpty() && delay < 100) {
            try {
                TimeUnit.MILLISECONDS.sleep(delay);
                delay <<= 1; // 指数增加等待时间
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    // 优雅关闭
    public void shutdown() {
        workers.shutdown();
        while (!queue.isEmpty()) {
            // 等待剩余任务处理完成
            Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
        }
    }
}

需要注意的是,以下场景不适合。

  • 阻塞需求,需要 take()阻塞等待。
  • 有界队列
  • 强一致性,要求精确的 size。

实现原理

以 JDK 17 源码为基线,ConcurrentLinkedQueue 是 Java 并发包中基于无锁算法实现的线程安全队列,专为高并发场景设计。其核心设计目标包括:

  • 无阻塞操作:通过 CAS 实现非阻塞算法。
  • 线性扩展能力:性能随 CPU 核心数增加而提升。
  • 弱一致性:迭代器与 size() 方法返回近似值。
  • 内存效率:每个元素仅需 24 字节存储开销。

ConcurrentLinkedQueue 的结构

ConcurrentLinkedQueue 由 head 节点和 tail 节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,节点与节点之间就是通过这个 next 关联起来,从而组成一张链表结构的队列。

ConcurrentLinkedQueue 的节点都是 Node 类型的。

kotlin 复制代码
static finalclass Node<E> {
    volatile E item;
    volatile Node<E> next;


    Node(E item) {
        ITEM.set(this, item);
    }


    Node() {}

    void appendRelaxed(Node<E> next) {

        NEXT.set(this, next);
    }

    boolean casItem(E cmp, E val) {
        return ITEM.compareAndSet(this, cmp, val);
    }
}

入队操作(offer)

流程图如下。

代码解析。

ini 复制代码
public boolean offer(E e) {
    final Node<E> newNode = new Node<E>(Objects.requireNonNull(e));

    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        if (q == null) {
            // CAS插入新节点
            if (NEXT.compareAndSet(p, null, newNode)) {
                // 惰性更新tail(允许失败)
                if (p != t)
                    TAIL.weakCompareAndSet(this, t, newNode);
                returntrue;
            }
        }
        elseif (p == q) // 处理已移除节点
            p = (t != (t = tail)) ? t : head;
        else// 推进指针
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

优化点

  • weakCompareAndSet 减少内存屏障开销。
  • 允许尾指针最多滞后 log(n) 个节点。
  • 通过 VarHandle 实现精确内存排序。

出队操作(poll)

核心机制:

  • 两阶段出队:先标记 item 为 null,再更新 head。
  • 头指针可能跳跃多个已消费节点。
  • 自动清理无效节点。

流程图如下。

核心源码。

ini 复制代码
public E poll() {
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            E item;
            if ((item = p.item) != null && p.casItem(item, null)) {
                // 成功获取数据
                if (p != h)
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            elseif ((q = p.next) == null) {
                updateHead(h, p);
                returnnull;
            }
            elseif (p == q)
              // 继续循环
                 continue restartFromHead;
        }
    }
}

Java 7 种阻塞队列

Chaya:"什么是阻塞队列?"

阻塞队列,顾名思义,首先它是一个队列,线程 1 往阻塞队列中添加元素,而线程 2 从阻塞队列中移除元素。

  • 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
  • 当阻塞队列是满时,从队列中添加元素的操作将会被阻塞。

JDK1.8 中的阻塞队列实现共有 7 个,分别是:

  • ArrayBlockingQueue:基于数组的有界队列。
  • LinkedBlockingQueue:基于链表的无界队列(可以设置容量)。
  • PriorityBlockingQueue:基于二叉堆的无界优先级队列。
  • DelayQueue:基于 PriorityBlockingQueue 的无界延迟队列。
  • SynchronousQueue:无容量的阻塞队列(Executors.newCachedThreadPool() 中使用的队列)。
  • LinkedTransferQueue:基于链表的无界队列。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

阻塞队列核心接口

阻塞队列统一实现了BlockingQueue接口,BlockingQueue接口在java.utilQueue接口的基础上提供了put(e)以及take()两个阻塞方法

除了阻塞功能,BlockingQueue 接口还定义了定时的offer以及poll,以及一次性移除方法drainTo

java 复制代码
//插入元素,队列满后会抛出异常
boolean add(E e);
//移除元素,队列为空时会抛出异常
E remove();

//插入元素,成功反会true
boolean offer(E e);
//移除元素
E poll();

//插入元素,队列满后会阻塞
void put(E e) throws InterruptedException;
//移除元素,队列空后会阻塞
E take() throws InterruptedException;

//限时插入
boolean offer(E e, long timeout, TimeUnit unit)
//限时移除
E poll(long timeout, TimeUnit unit);

//获取所有元素到Collection中
int drainTo(Collection<? super E> c);

阻塞队列 6 大使用场景

Java 阻塞队列(BlockingQueue)是并发编程中的核心工具,其线程安全阻塞特性使其在以下场景中发挥重要作用。

生产者-消费者模型(经典场景)

电商系统中,用户下单后需异步处理库存扣减、支付回调、物流通知等操作。

痛点:生产(下单)与消费(处理)速度不一致,需解耦并保证高吞吐。

scss 复制代码
public class OrderProcessor {
    // 生产级配置:建议队列大小为 CPU 核心数*2~4
    privatestaticfinal BlockingQueue<Order> queue = new LinkedBlockingQueue<>(2048);
    privatestaticfinal ExecutorService consumerPool = Executors.newFixedThreadPool(8);

    // 生产者(Web 服务线程)
    public void submitOrder(Order order) {
        if (!queue.offer(order)) {  // 队列满时快速失败
            log.warn("Order queue overflow! Reject order: {}", order.getId());
            thrownew ServiceException("系统繁忙,请稍后重试");
        }
        log.info("Order submitted: {}", order.getId());
    }

    // 消费者(后台线程池)
    @PostConstruct
    public void initConsumers() {
        for (int i = 0; i < 8; i++) {
            consumerPool.execute(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        Order order = queue.take();  // 阻塞直到有订单
                        processOrder(order);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            });
        }
    }

    private void processOrder(Order order) {
        // 扣减库存、支付回调等业务逻辑
    }
}

线程池任务调度

场景问题

视频平台需将上传的视频转码为不同分辨率,任务具有突发性。

arduino 复制代码
public class VideoTranscoder {
    // 使用 PriorityBlockingQueue 确保 VIP 用户优先处理
    privatestaticfinal BlockingQueue<TranscodeTask> queue =
        new PriorityBlockingQueue<>(1000, Comparator.comparing(TranscodeTask::getPriority));

    // 自定义线程池(核心线程数=CPU数, 最大线程数=CPU数*2)
    privatestaticfinal ThreadPoolExecutor executor = new ThreadPoolExecutor(
        4, 8, 30, TimeUnit.SECONDS, queue
    );

    public void submitTranscodeTask(TranscodeTask task) {
        executor.execute(() -> {
            // 执行实际转码操作
            process(task);
        });
    }

    // 监控队列状态(生产环境建议接入 Prometheus)
    public MonitorData getQueueStatus() {
        returnnew MonitorData(
            queue.size(),
            executor.getActiveCount(),
            queue.remainingCapacity()
        );
    }
}

生产级要点:

  • 使用有界队列避免 OOM。
  • RejectedExecutionHandler 需配置合理拒绝策略。
  • 队列监控接入告警系统。

流量削峰

瞬时流量高峰可达平时 100 倍,数据库无法承受直接压力。

arduino 复制代码
public class SeckillService {
    // 队列容量=商品库存*2(内存可控)
    privatefinal BlockingQueue<SeckillRequest> queue =
        new ArrayBlockingQueue<>(20000);

    // 异步消费队列
    @Scheduled(fixedRate = 100)
    public void processQueue() {
        List<SeckillRequest> batch = new ArrayList<>(100);
        queue.drainTo(batch, 100);  // 批量取100条
        if (!batch.isEmpty()) {
            seckillDao.batchProcess(batch); // 批量写入数据库
        }
    }

    public boolean trySeckill(SeckillRequest request) {
        return queue.offer(request);  // 非阻塞提交
    }
}

生产级设计:

  • 队列容量与数据库吞吐量匹配。
  • 批量处理减少数据库压力。
  • 前端配合显示排队状态。

延迟任务调度:订单超时关闭

需在订单创建 30 分钟后检查支付状态,未支付自动关闭。

typescript 复制代码
public class OrderTimeoutChecker implements Runnable {
    privatefinal DelayQueue<DelayedOrder> queue = new DelayQueue<>();

    public void addOrder(Order order) {
        queue.put(new DelayedOrder(order, 30, TimeUnit.MINUTES));
    }

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                DelayedOrder order = queue.take();  // 阻塞直到有到期订单
                checkPayment(order.getOrderId());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    privatestaticclass DelayedOrder implements Delayed {
        privatefinal Order order;
        privatefinallong expireTime;

        // 实现 getDelay() 和 compareTo()
    }
}

生产级注意:

  • 分布式场景需用 Redis/ZooKeeper 替代。
  • 集群环境下需防重复处理。
  • 添加 JVM 关闭钩子确保任务不丢失。

异步日志系统

需要记录详细业务日志但磁盘 I/O 不能影响主线程性能。

csharp 复制代码
public class AsyncLogger {
    privatestaticfinal BlockingQueue<LogEvent> queue =
        new LinkedTransferQueue<>();  // 高吞吐无界队列

    static {
        // 守护线程消费日志
        Thread loggerThread = new Thread(() -> {
            while (true) {
                try {
                    LogEvent event = queue.take();
                    writeToDisk(event);
                } catch (InterruptedException e) {
                    // 优雅关闭处理
                    drainRemainingLogs();
                    break;
                }
            }
        });
        loggerThread.setDaemon(true);
        loggerThread.start();
    }

    public static void log(LogEvent event) {
        if (!queue.offer(event)) {  // 防御性设计
            fallbackLog(event);
        }
    }
}

线程池队列

线程池中活跃线程数达到 corePoolSize 时,线程池将会将后续的 task 提交到 BlockingQueue 中。

线程池的核心方法 ThreadPoolExecutor,用 BlockingQueue 存放任务的阻塞队列,被提交但尚未被执行的任务。

java 复制代码
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

线程池在内部实际也是构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。

不同的线程池实现用的是不同的阻塞队列,newFixedThreadPool 和 newSingleThreadExecutor 用的是 LinkedBlockingQueue,newCachedThreadPool 用的是 SynchronousQueue。

各类队列对比和选型

生产选型建议:

  • 网络请求缓冲ArrayBlockingQueue(可控内存)
  • 任务调度PriorityBlockingQueue(优先级控制)
  • 线程间直接通信SynchronousQueue
  • 磁盘 I/O 解耦LinkedBlockingQueue(吞吐优先)

性能优化要点

队列监控:

arduino 复制代码
// 通过 JMX 暴露指标
public class QueueMonitor implements QueueMonitorMXBean {
    private final BlockingQueue<?> queue;

    public int getQueueSize() {
        return queue.size();
    }

    // 注册到 MBeanServer...
}

拒绝策略(以线程池为例):

typescript 复制代码
new ThreadPoolExecutor.CallerRunsPolicy() {  // 生产推荐策略
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        log.warn("Task rejected, running in caller thread");
        if (!e.isShutdown()) {
            r.run();
        }
    }
}

动态扩缩容:

arduino 复制代码
// 根据监控指标调整队列容量
public void adjustQueueCapacity(int newSize) {
    if (queue instanceof ResizableBlockingQueue) {
        ((ResizableBlockingQueue) queue).setCapacity(newSize);
    }
}

掌握了各种阻塞队列的使用场景,接下来我们深入拆解每个阻塞队列的实现原理。

ArrayBlockingQueue

ArrayBlockingQueue是一个底层用数组实现的有界阻塞队列,有界是指他的容量大小是固定的,不能扩充容量,在初始化时就必须确定队列大小。

它通过可重入的独占锁ReentrantLock来控制并发,Condition来实现阻塞和通知唤醒。

结构概述

scala 复制代码
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
    final Object[] items;               // 容器数组
    int takeIndex;                      // 出队索引
    int putIndex;                       // 入队索引
    int count;                          // 排队个数
    final ReentrantLock lock;           // 全局锁
    private final Condition notEmpty;   // 出队条件队列
    private final Condition notFull;    // 入队条件队列
    ...
}

ArrayBlockingQueue 的结构如图所示:

  • ArrayBlockingQueue 的数组其实是一个逻辑上的环状结构,在添加、取出数据的时候,并没有像 ArrayList 一样发生数组元素的移动(当然除了 removeAt(final int removeIndex))。
  • 并且由 takeIndexputIndex 指示读写位置。
  • 在读写的时候还有两个读写条件队列。

阻塞入队

阻塞入队 put 方法:

ini 复制代码
public void put(E e) throws InterruptedException {
    Objects.requireNonNull(e);
    final ReentrantLock lock = this.lock;
    //获取独占锁
    lock.lockInterruptibly();
    try {
      //如果队列已满则通过await阻塞put方法
        while (count == items.length)
            notFull.await();
        //满足条件,插入元素,并唤醒因notEmpty等待的消费线程
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
//插入元素后将putIndex+1,当队列使用完后重置为0
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
//队列添加元素后唤醒因notEmpty等待的消费线程
    notEmpty.signal();
}

阻塞出队

ini 复制代码
//移除队列中的元素
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
//获取独占锁
    lock.lockInterruptibly();
    try {
      //如果队列已空则通过await阻塞take方法
        while (count == 0)
            notEmpty.await();
        return dequeue(); //移除元素
    } finally {
        lock.unlock();
    }
}

private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
//移除元素后将takeIndex+1,当队列使用完后重置为0
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
//队列消费元素后唤醒因notFull等待的消费线程
    notFull.signal();
    return x;
}

队列满后通过notFull.await()来阻塞生产者线程,消费元素后通过 notFull.signal()来唤醒阻塞的生产者线程。

队列为空后通过notEmpty.await()来阻塞消费者线程,生产元素后通过notEmpty.signal()唤醒阻塞的消费者线程。

drainTo

drainTo方法可以一次性获取队列中所有的元素,它减少了锁定队列的次数,使用得当在某些场景下对性能有不错的提升。

ini 复制代码
public int drainTo(Collection<? super E> c, int maxElements) {
    checkNotNull(c);
    if (c == this)
        thrownew IllegalArgumentException();
    if (maxElements <= 0)
        return0;
    final Object[] items = this.items;
    final ReentrantLock lock = this.lock; //仅获取一次锁
    lock.lock();
    try {
        int n = Math.min(maxElements, count); //获取队列中所有元素
        int take = takeIndex;
        int i = 0;
        try {
            while (i < n) {
                @SuppressWarnings("unchecked")
                E x = (E) items[take];
                c.add(x); //循环插入元素
                items[take] = null;
                if (++take == items.length)
                    take = 0;
                i++;
            }
            return n;
        } finally {
            // Restore invariants even if c.add() threw
            if (i > 0) {
                count -= i;
                takeIndex = take;
                if (itrs != null) {
                    if (count == 0)
                        itrs.queueIsEmpty();
                    elseif (i > take)
                        itrs.takeIndexWrapped();
                }
                for (; i > 0 && lock.hasWaiters(notFull); i--)
                    notFull.signal(); //唤醒等待的生产者线程
            }
        }
    } finally {
        lock.unlock();
    }
}

LinkedBlockingQueue

LinkedBlockingQueue是一个底层用单向链表实现的有界阻塞队列,和ArrayBlockingQueue一样,采用ReentrantLock来控制并发,不同的是它使用了两个独占锁来控制消费和生产。

如果不是特殊业务,LinkedBlockingQueue 使用时,切记要定义容量 new LinkedBlockingQueue(capacity),防止过度膨胀。

结构概述

ini 复制代码
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
// 默认 Integer.MAX_VALUE
privatefinalint capacity;
// 容量
privatefinal AtomicInteger count = new AtomicInteger();
// 头结点 head.item == null
transient Node<E> head;
// 尾节点 last.next == null
privatetransient Node<E> last;
// take锁,出队锁,只有take,poll方法会持有
privatefinal ReentrantLock takeLock = new ReentrantLock();
// 出队等待条件
// 当队列无元素时,take锁会阻塞在notEmpty条件上,等待其它线程唤醒
privatefinal Condition notEmpty = takeLock.newCondition();
// 入队锁,只有put,offer会持有
privatefinal ReentrantLock putLock = new ReentrantLock();
// 入队等待条件
// 当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒
privatefinal Condition notFull = putLock.newCondition();
// 基于链表实现,肯定要有结点类,典型的单链表结构
staticclass Node<E> {
    E item;
    Node<E> next;
    Node(E x) { item = x; }
  }
}

LinkedBlockingQueue 的结构如图所示:

如图所示,

  • LinkedBlockingQueue 其实就是一个简单的单向链表,其中头部元素的数据为空,尾部元素的 next 为空。
  • 因为读写都有竞争,所以在头部和尾部分别有一把锁;同时还有对应的两个条件队列。

put 和 take 方法

ini 复制代码
public void put(E e) throws InterruptedException {
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    //因为使用了双锁,需要使用AtomicInteger计算元素总量,避免并发计算不准确
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        while (count.get() == capacity) {
            //队列已满,阻塞生产线程
            notFull.await();
        }
      //插入元素到队列尾部
        enqueue(node);
      //count + 1
        c = count.getAndIncrement();
      //如果+1后队列还未满,通过其他生产线程继续生产
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
//只有当之前是空时,消费队列才会阻塞,否则是不需要通知的
    if (c == 0)
        signalNotEmpty();
}

private void enqueue(Node<E> node) {
    //将新元素添加到链表末尾,然后将last指向尾部元素
    last = last.next = node;
}

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
          //队列为空,阻塞消费线程
            notEmpty.await();
        }
      //消费一个元素
        x = dequeue();
      //count - 1
        c = count.getAndDecrement();
      // 通知其他等待的消费线程继续消费
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    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;
}

可以看到LinkedBlockingQueue通过takeLockputLock两个锁来控制生产和消费,互不干扰,只要队列未满,生产线程可以一直生产,只要队列不为空,消费线程可以一直消费,不会相互因为独占锁而阻塞。

Chaya:"为什么 ArrayBlockingQueue 中不使用双锁来实现队列的生产和消费呢?"

我的理解是 ArrayBlockingQueue 也能使用双锁来实现功能,但由于它底层使用了数组这种简单结构,相当于一个共享变量,如果通过两个锁,需要更加精确的锁控制。

LinkedBlockingQueue不存在这个问题,链表这种数据结构头尾节点都相对独立,存储上也不连续,双锁控制不存在复杂性。

PriorityBlockingQueue

PriorityBlockingQueue是一个底层由数组实现的无界队列,并带有排序功能,同样采用ReentrantLock来控制并发。

由于是无界的,所以插入元素时不会阻塞,没有队列满的状态,只有队列为空的状态。

PriorityBlockingQueue 具有以下特性:

  • 基于优先级排序 :元素按自然顺序(Comparable)或自定义 Comparator 排序。
  • 线程安全 :通过 ReentrantLock 保证并发操作的安全性。
  • 动态扩容:底层是数组实现的二叉堆,容量不足时自动扩容。
  • 阻塞操作take() 在队列为空时阻塞;put() 不会阻塞(因为队列无界)。

适用场景

例子中,创建了一个优先级阻塞队列,用于存储和检索PriorityTask对象,这些对象根据它们的优先级进行排序,client 代码会向队列中添加任务,并从队列中检索并处理优先级最高的任务。

arduino 复制代码
// 定义一个具有优先级的任务类
class PriorityTask implements Comparable<PriorityTask>{
    privateint priority;
    private String name;

    public PriorityTask(int priority, String name) {
        this.priority = priority;
        this.name = name;
    }

    @Override
    public int compareTo(PriorityTask o) {
        if (this.priority < o.priority) {
            return -1;
        } elseif (this.priority > o.priority) {
            return1;
        } else {
            return0;
        }
    }

    @Override
    public String toString() {
        return"PriorityTask{" +
                "priority=" + priority +
                ", name='" + name + ''' +
                '}';
    }
}


publicclass PriorityBlockingQueueExample {

    public static void main(String[] args) throws InterruptedException {
        // 创建一个优先级阻塞队列,使用PriorityTask类的自然顺序进行排序
        PriorityBlockingQueue<PriorityTask> queue = new PriorityBlockingQueue<>();

        // 添加任务到队列
        queue.put(new PriorityTask(3, "Task 3"));
        queue.put(new PriorityTask(1, "Task 1"));
        queue.put(new PriorityTask(2, "Task 2"));

        // 从队列中取出并打印任务,优先级高的先出队
        while (!queue.isEmpty()) {
            PriorityTask task = queue.take();
            System.out.println("Processing: " + task);
        }
    }

}

在这个示例中,定义了一个名为 PriorityTask 的类,它实现了 Comparable 接口,并且重写了 compareTo 方法来定义优先级规则。

队列中的元素将根据这个规则自动排序,从而保证优先级高的任务先被处理。

实现原理

PriorityBlockingQueue 内部通过 数组 维护一个 最小二叉堆(默认),堆顶元素始终是优先级最高的(最小元素)。 数组下标关系:

  • 父节点:parent = (childIndex - 1) / 2
  • 左子节点:leftChild = parent * 2 + 1
  • 右子节点:rightChild = parent * 2 + 2
关键字段
java 复制代码
// 存储元素的数组
private transient Object[] queue;

// 元素数量
private transientint size;

// 排序规则(为 null 时使用自然排序)
private transient Comparator<? super E> comparator;

// 保证线程安全的锁
private final ReentrantLock lock = new ReentrantLock();

// 非空条件变量(用于 take() 阻塞)
private final Condition notEmpty = lock.newCondition();
入队源码
scss 复制代码
public boolean offer(E e) {
    if (e == null)
        thrownew NullPointerException();
   // 首先获取锁对象。
    final ReentrantLock lock = this.lock;
   // 只有一个线程操作入队和出队动作。
    lock.lock();
   // n代表数组的实际存储内容的大小
   // cap代表队列的整体大小,也就是数组的长度。
    int n, cap;
    Object[] array;
   // 如果数组实际长度大于等于数组的长度时,需要进行扩容操作。
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap);
    try {
       // 如果用户指定比较器,则使用用户指定的比较器来进行比较,如果没有则使用默认比较器。
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
           // 进行上浮操作。
            siftUpComparable(n, e, array);
        else
           // 进行上浮操作。
            siftUpUsingComparator(n, e, array, cmp);
       // 实际长度增加1,由于有且仅有一个线程操作队列,所以这里并没有使用原子性操作。
        size = n + 1;
       // 通知等待的线程,队列已经有数据,可以获取数据。
        notEmpty.signal();
    } finally {
       // 解锁操作。
        lock.unlock();
    }
   // 返回操作成功。
    returntrue;
}
关键步骤
  • 加锁:确保线程安全。
  • 扩容检查 :若数组已满,调用 tryGrow() 扩容(通常扩容 50%)。
  • 堆上浮:将新元素插入数组末尾,逐步与父节点比较并交换,直到满足堆性质。
  • 唤醒消费者 :通知可能阻塞的 take() 线程。
出队
ini 复制代码
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    E result;
    try {
        while ( (result = dequeue()) == null)
            notEmpty.await();
    } finally {
        lock.unlock();
    }
    return result;
}

private E dequeue() {
   // 数组的元素的个数。
    int n = size - 1;
   // 如果数组中不存在元素则直接返回null。
    if (n < 0)
        returnnull;
    else {
       // 获取队列数组。
        Object[] array = queue;
       // 将第一个元素也就是二叉堆的根结点堆顶元素作为返回结果。
        E result = (E) array[0];
       // 获取数组中最后一个元素。
        E x = (E) array[n];
       // 将最后一个元素设置为null。
        array[n] = null;
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
           // 进行下沉操作。
            siftDownComparable(0, x, array, n);
        else
           // 进行下沉操作。
            siftDownUsingComparator(0, x, array, n, cmp);
       // 实际元素大小减少1.
        size = n;
       // 返回结果。
        return result;
    }
}
关键步骤
  • 加锁:确保线程安全。
  • 取出堆顶:返回堆顶元素(优先级最高)。
  • 堆下沉:将末尾元素移到堆顶,逐步与子节点比较并交换,直到满足堆性质。

DelayQueue

DelayQueue 也是一个无界队列,它是在PriorityQueue基础上实现的,先按延迟优先级排序,延迟时间短的排在前面

PriorityBlockingQueue相似,底层也是数组,采用一个ReentrantLock来控制并发。

由于是无界的,所以插入元素时不会阻塞,没有队列满的状态。

ini 复制代码
private finaltransient ReentrantLock lock = new ReentrantLock();
privatefinal PriorityQueue<E> q = new PriorityQueue<E>();//优先级队列

public void put(E e) {
    offer(e);
}

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        q.offer(e); //插入元素到优先级队列
        if (q.peek() == e) { //如果插入的元素在队列头部
            leader = null;
            available.signal(); //通知消费线程
        }
        returntrue;
    } finally {
        lock.unlock();
    }
}

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
            E first = q.peek(); //获取头部元素
            if (first == null)
                available.await(); //空队列阻塞
            else {
                long delay = first.getDelay(NANOSECONDS); //检查元素是否延迟到期
                if (delay <= 0)
                    return q.poll(); //到期则弹出元素
                first = null; // don't retain ref while waiting
                if (leader != null)
                    available.await();
                else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        available.awaitNanos(delay); //阻塞未到期的时间
                    } finally {
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
            available.signal();
        lock.unlock();
    }
}

SynchronousQueue

SynchronousQueue相比较之前的 4 个队列就比较特殊了,它是一个没有容量的队列,也就是说它内部时不会对数据进行存储,每进行一次 put 之后必须要进行一次 take,否则相同线程继续 put 会阻塞。

这种特性很适合做一些传递性的工作,一个线程生产,一个线程消费。

这里只对它的非公平实现下的 take 和 put 方法做下简单分析:

scss 复制代码
//非公平情况下调用内部类TransferStack的transfer方法put
public void put(E e) throws InterruptedException {
    if (e == null) thrownew NullPointerException();
    if (transferer.transfer(e, false, 0) == null) {
        Thread.interrupted();
        thrownew InterruptedException();
    }
}
//非公平情况下调用内部类TransferStack的transfer方法take
public E take() throws InterruptedException {
    E e = transferer.transfer(null, false, 0);
    if (e != null)
        return e;
    Thread.interrupted();
    thrownew InterruptedException();
}

//具体的put以及take方法,只有E的区别,通过E来区别REQUEST还是DATA模式
E transfer(E e, boolean timed, long nanos) {
    SNode s = null; // constructed/reused as needed
    int mode = (e == null) ? REQUEST : DATA;

    for (;;) {
        SNode h = head;
        //栈无元素或者元素和插入的元素模式相匹配,也就是说都是插入元素
        if (h == null || h.mode == mode) {
            //有时间限制并且超时
            if (timed && nanos <= 0) {
                if (h != null && h.isCancelled())
                    casHead(h, h.next);  // 重新设置头节点
                else
                    returnnull;
            }
            //未超时cas操作尝试设置头节点
            elseif (casHead(h, s = snode(s, e, h, mode))) {
                //自旋一段时间后未消费元素则挂起put线程
                SNode m = awaitFulfill(s, timed, nanos);
                if (m == s) {               // wait was cancelled
                    clean(s);
                    returnnull;
                }
                if ((h = head) != null && h.next == s)
                    casHead(h, s.next);     // help s's fulfiller
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
        }
        //栈不为空并且和头节点模式不匹配,存在元素则消费元素并重新设置head节点
        elseif (!isFulfilling(h.mode)) { // try to fulfill
            if (h.isCancelled())            // already cancelled
                casHead(h, h.next);         // pop and retry
            elseif (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                for (;;) { // loop until matched or waiters disappear
                    SNode m = s.next;       // m is s's match
                    if (m == null) {        // all waiters are gone
                        casHead(s, null);   // pop fulfill node
                        s = null;           // use new node next time
                        break;              // restart main loop
                    }
                    SNode mn = m.next;
                    if (m.tryMatch(s)) {
                        casHead(s, mn);     // pop both s and m
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else                  // lost match
                        s.casNext(m, mn);   // help unlink
                }
            }
        }
        //节点正在匹配阶段
        else {                            // help a fulfiller
            SNode m = h.next;               // m is h's match
            if (m == null)                  // waiter is gone
                casHead(h, null);           // pop fulfilling node
            else {
                SNode mn = m.next;
                if (m.tryMatch(h))          // help match
                    casHead(h, mn);         // pop both h and m
                else                        // lost match
                    h.casNext(m, mn);       // help unlink
            }
        }
    }
}

//先自旋后挂起的核心方法
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
    finallong deadline = timed ? System.nanoTime() + nanos : 0L;
    Thread w = Thread.currentThread();
    //计算自旋的次数
    int spins = (shouldSpin(s) ?
                    (timed ? maxTimedSpins : maxUntimedSpins) : 0);
    for (;;) {
        if (w.isInterrupted())
            s.tryCancel();
        SNode m = s.match;
        //匹配成功过返回节点
        if (m != null)
            return m;
        //超时控制
        if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                s.tryCancel();
                continue;
            }
        }
        //自旋检查,是否进行下一次自旋
        if (spins > 0)
            spins = shouldSpin(s) ? (spins-1) : 0;
        elseif (s.waiter == null)
            s.waiter = w; // establish waiter so can park next iter
        elseif (!timed)
            LockSupport.park(this); //在这里挂起线程
        elseif (nanos > spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}

代码非常复杂,这里说下我所理解的核心逻辑。

代码中可以看到put以及take方法都是通过调用transfer方法来实现的,然后通过参数mode来区别,在生产元素时如果是同一个线程多次put则会采取自旋的方式多次尝试put元素,可能自旋过程中元素会被消费,这样可以及时put,降低线程挂起的性能损耗,高吞吐量的核心也在这里.

消费线程一样,空栈时也会先自旋,自旋失败然后通过线程的LockSupport.park方法挂起。

LinkedTransferQueue

LinkedTransferQueue 是一个由链表结构组成的无界阻塞 TransferQueue 队列。

LinkedTransferQueue采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为 null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为 null 的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。我们称这种节点操作为"匹配"方式。

队列实现了 TransferQueue 接口重写了 tryTransfer 和transfer方法,这组方法和SynchronousQueue` 公平模式的队列类似,具有匹配的功能.。

LinkedBlockingDeque

LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列。

所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。

相比其他的阻塞队列,LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast 等方法,以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。

以 Last 单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。

另外插入方法 add 等同于 addLast,移除方法 remove 等效于 removeFirst。

在初始化 LinkedBlockingDeque 时可以设置容量防止其过渡膨胀,默认容量也是 Integer.MAX_VALUE。

另外双向阻塞队列可以运用在"工作窃取"模式中。

相关推荐
LI JS@你猜啊8 分钟前
window安装docker
java·spring cloud·eureka
书中自有妍如玉17 分钟前
.net 使用MQTT订阅消息
java·前端·.net
风铃儿~43 分钟前
Spring AI 入门:Java 开发者的生成式 AI 实践之路
java·人工智能·spring
斯普信专业组1 小时前
Tomcat全方位监控实施方案指南
java·tomcat
忆雾屿1 小时前
云原生时代 Kafka 深度实践:06原理剖析与源码解读
java·后端·云原生·kafka
武昌库里写JAVA1 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
gaoliheng0061 小时前
Redis看门狗机制
java·数据库·redis
我是唐青枫1 小时前
.NET AOT 详解
java·服务器·.net
Su米苏2 小时前
Axios请求超时重发机制
java
Undoom2 小时前
🔥支付宝百宝箱新体验!途韵归旅小帮手,让高铁归途变旅行
后端