Java并发容器框架

ConcurrentHashMap 的使用

@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 方法实现对计数器的增加操作。

    • 如果 key 不存在则新建一个值为 1 的计数器;

    • 否则将其 value 递增 1。

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

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

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;  
        }  
    }  
}  

ConcurrentHashMap实现原理

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

JDK 1.7 版本 ConcurrentHashMap

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

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

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

  1. 首先计算 key 的哈希值;

  2. 根据哈希值找到对应的 Segment;

  3. 获取 Segment 对应的锁;

  4. 如果还没有元素,就直接插入到 Segment 中;

  5. 如果已经存在元素,就循环比较 key 是否相等;

  6. 如果 key 已经存在,就根据要求更新 value;

  7. 如果 key 不存在,就插入新的元素(链表或者红黑树)。

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

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

JDK 1.8 ConcurrentHashMap

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

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

  • 使用 Unsafe 方法操作数组内部元素,保证可见性;(U.getObjectVolatile、U.compareAndSwapObject、U.putObjectVolatile);

  • 在更新和移动节点的时候,直接锁住对应的哈希桶,锁粒度更小,且动态扩展;

  • 针对扩容慢操作进行优化,

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

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

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

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

  • 优化哈希表计数器,采用 LongAdder、Striped64 类似思想;

  • 以及大量的哈希算法优化和状态变量优化;

类定义和成员变量
// 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 :

    • table 未初始化,表示初始化容量;

    • table 已初始化,表示扩容阈值(0.75n);

  • sizeCtl = -1 :表示正在初始化;

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

Node 节点

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

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;  
哈希计算

 

static finalint MOVED     = -1;          // hash for forwarding nodes  
static finalint TREEBIN   = -2;          // hash for roots of trees  
static finalint RESERVED  = -3;          // hash for transient reservations  
static finalint 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 方法来保证:

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 成功,可以分成以下六步流程来概述。

  1. 如果没有初始化就先调用 initTable()方法来进行初始化过程

  2. 如果没有 hash 冲突就直接 CAS 插入

  3. 如果还在进行扩容操作就先进行扩容

  4. 如果存在 hash 冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,

  5. 最后一个如果该链表的数量大于阈值 8,就要先转换成黑红树的结构,break 再一次进入循环

  6. 如果添加成功就调用 addCount()方法统计 size,并且检查是否需要扩容

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,并且检查是否需要扩容  
    return null;  
}  

 流程图如下所示:

get 操作

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

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 操作的流程很简单,也很清晰,可以分为三个步骤来描述.

  1. 计算 hash 值,定位到该 table 索引位置,如果是首节点符合就返回

  2. 如果遇到扩容的时候,会调用标志正在扩容节点 ForwardingNode 的 find 方法,查找该节点,匹配就返回

  3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回 null

size 操作

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

扩容

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

另外扩容条件也有两个:

  • 有链表长度超过 8,但是容量小于 64 的时候,发生扩容;

  • 节点数超过阈值的时候,发生扩容;

其扩容的过程可描述为:

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

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

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

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

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(先进先出)原则对元素进行排序。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。

// 订单事件处理器(生产环境级实现)  
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);  
        }  
    }  
}  

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

  1. 阻塞需求,需要 take()阻塞等待。

  2. 有界队列

  3. 强一致性,要求精确的 size。

实现原理

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

  1. 无阻塞操作 :通过 CAS 实现非阻塞算法

  2. 线性扩展能力 :性能随 CPU 核心数增加而提升

  3. 弱一致性 :迭代器与 size() 方法返回近似值

  4. 内存效率 :每个元素仅需 24 字节存储开销

ConcurrentLinkedQueue 的结构

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

ConcurrentLinkedQueue 的节点都是 Node 类型的。

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)

流程图如下。

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;
    }
}

优化点

  1. weakCompareAndSet 减少内存屏障开销

  2. 允许尾指针最多滞后 log(n) 个节点

  3. 通过 VarHandle 实现精确内存排序

出队操作(poll)

核心机制

  1. 两阶段出队:先标记 item 为 null,再更新 head

  2. 头指针可能跳跃多个已消费节点

  3. 自动清理无效节点

流程图如下。

核心源码。

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 种阻塞队列

阻塞队列,顾名思义,首先它是一个队列,线程 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

//插入元素,队列满后会抛出异常  
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)是并发编程中的核心工具,其 线程安全阻塞特性 使其在以下场景中发挥重要作用。

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

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

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

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) {  
        // 扣减库存、支付回调等业务逻辑  
    }  
}  

线程池任务调度
场景问题

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

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()  
        );  
    }  
}  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()  
        );  
    }  
}  

生产级要点

  1. 使用有界队列避免 OOM

  2. RejectedExecutionHandler 需配置合理拒绝策略

  3. 队列监控接入告警系统

流量削峰

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

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);  // 非阻塞提交
    }
}

生产级设计

  1. 队列容量与数据库吞吐量匹配

  2. 批量处理减少数据库压力

  3. 前端配合显示排队状态

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

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

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()
    }
}

生产级注意

  1. 分布式场景需用 Redis/ZooKeeper 替代

  2. 集群环境下需防重复处理

  3. 添加 JVM 关闭钩子确保任务不丢失

异步日志系统

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

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 存放任务的阻塞队列,被提交但尚未被执行的任务。

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

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

性能优化要点

队列监控

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

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

    // 注册到 MBeanServer...
}

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

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

动态扩缩容

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

ArrayBlockingQueue

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

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

结构概述
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;    // 入队条件队列
    ...
}

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();
}
阻塞出队
//移除队列中的元素
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方法可以一次性获取队列中所有的元素,它减少了锁定队列的次数,使用得当在某些场景下对性能有不错的提升。

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),防止过度膨胀。

结构概述
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; }
  }
}

put 和 take 方法
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 代码会向队列中添加任务,并从队列中检索并处理优先级最高的任务。

// 定义一个具有优先级的任务类
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

// 存储元素的数组
privatetransient Object[] queue;

// 元素数量
privatetransientint size;

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

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

// 非空条件变量(用于 take() 阻塞)
privatefinal Condition notEmpty = lock.newCondition();
入队源码
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() 线程。

出队
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来控制并发。

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

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 方法做下简单分析:

//非公平情况下调用内部类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。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值