图解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。

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

相关推荐
LCY1334 分钟前
spring security +kotlin 实现oauth2.0 认证
java·spring·kotlin
soulermax6 分钟前
数字ic后端设计从入门到精通2(含fusion compiler, tcl教学)
java·linux·服务器
我的代码永没有bug11 分钟前
day1-小白学习JAVA---JDK安装和环境变量配置(mac版)
java·学习·macos
Chandler2418 分钟前
Go:反射
开发语言·后端·golang
王有品22 分钟前
Spring MVC 一个简单的多文件上传
java·spring·mvc
pwzs25 分钟前
深入浅出 MVCC:MySQL 并发背后的多版本世界
数据库·后端·mysql
盒子691025 分钟前
go for 闭环问题【踩坑记录】
开发语言·后端·golang
Johnny Lnex1 小时前
JVM之经典垃圾回收器
java
chat2tomorrow1 小时前
数据仓库 vs 数据湖:架构、应用场景与技术差异全解析
大数据·数据仓库·低代码·架构·数据湖·sql2api
那就摆吧1 小时前
数据结构-栈
android·java·c语言·数据结构