🏥 故事:优先级急诊医院 (PriorityBlockingQueue)

115 阅读8分钟

揭开Java中最智能的队列——PriorityBlockingQueue(优先级阻塞队列)的神秘面纱!这次我们把它想象成一家"智能急诊医院",病人不是先来先治,而是按病情严重程度(优先级)决定救治顺序。准备好进入这个充满堆排序、动态扩容和优先级较量的世界了吗?🏥⚕️


🏥 故事舞台:智能急诊医院 (PriorityBlockingQueue)

​医院特点:​

  1. ​VIP优先制​​:病人按病情严重程度(优先级)排序,最紧急的(优先级最高)最先救治
  2. ​动态扩容病房​​:当病房不够时,医院会自动扩建(数组扩容)
  3. ​院长坐镇​​:只有一位院长有万能钥匙🔑 (ReentrantLock lock),所有关键操作都要他批准
  4. ​急救广播系统​​:当有病人来时,通过广播 (notEmpty) 通知医生

​核心设备:​

java
Copy
// 病房楼(数组实现的小顶堆)
private transient Object[] queue; 
// 当前病人数
private transient int size;
// 病情评估系统(Comparator决定谁更优先)
private transient Comparator<? super E> comparator;
// 院长的万能钥匙(全局锁)
private final ReentrantLock lock;
// 急救广播(Condition通知医生)
private final Condition notEmpty;
// 扩建许可证(0/1表示是否正在扩建)
private transient volatile int allocationSpinLock;

📌 ​​关键设计:​​ 医院用了一种叫 ​​"小顶堆" (Min-Heap)​​ 的神奇结构管理病房,保证病情最紧急(优先级数值最小)的病人永远在1号病房(堆顶)!


🎬 场景一:病人挂号入院(offer/put操作)

java
Copy
public boolean offer(E e) {
    if (e == null) throw new NullPointerException(); // 不收治"隐形病人"
    final ReentrantLock lock = this.lock;
    lock.lock(); // 1. 院长亲自上锁(不可中断锁)
    int n, cap;
    Object[] array;
    
    // 2. 检查病房是否已满?
    while ((n = size) >= (cap = (array = queue).length)) 
        tryGrow(array, cap); // 3. 满了就扩建病房!
    
    try {
        Comparator<? super E> cmp = comparator;
        if (cmp == null) // 4. 没有病情评估系统?按默认规则
            siftUpComparable(n, e, array);
        else // 5. 有评估系统?按系统规则
            siftUpUsingComparator(n, e, array, cmp);
        size = n + 1; // 6. 病人数+1
        notEmpty.signal(); // 7. 广播:"有病人!"
    } finally {
        lock.unlock(); // 8. 院长解锁
    }
    return true;
}

// 神奇扩建术
private void tryGrow(Object[] array, int oldCap) {
    lock.unlock(); // ⚡ 神奇操作:先释放锁!(允许其他操作继续)
    Object[] newArray = null;
    
    // 用"扩建许可证"确保只有一个工队在扩建
    if (allocationSpinLock == 0 && 
        UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) {
        try {
            // 计算新病房数量:小医院翻倍+2,大医院扩50%
            int newCap = oldCap + ((oldCap < 64) ? 
                                  (oldCap + 2) : 
                                  (oldCap >> 1));
            if (newCap > MAX_ARRAY_SIZE) // 处理超大医院
                newCap = hugeCapacity(oldCap);
            newArray = new Object[newCap]; // 建新病房楼
        } finally {
            allocationSpinLock = 0; // 归还许可证
        }
    }
    
    // 其他工队主动让出资源
    Thread.yield(); 
    lock.lock(); // 重新上锁
    
    if (newArray != null && queue == array) {
        queue = newArray; // 切换新病房楼
        System.arraycopy(array, 0, newArray, 0, oldCap); // 搬运病人
    }
}

​🤒 故事细节:​

  1. 心脏病人张三(优先级1)来挂号,被拒收"隐形病人"(非空检查)

  2. 院长亲自坐镇(lock.lock()

  3. ​关键检查​​:当前病人数 size >= 病房数 queue.length

    • ✅ 病房充足:直接入住
    • ❌ 病房已满:启动​​神奇扩建术​
  4. ​扩建魔术四步曲​​:

    • ​先解锁​​:院长暂时离开(lock.unlock()),让医生继续救治病人

    • ​抢许可证​​:工队用 CAS 抢"扩建许可证"(allocationSpinLock 0→1

    • ​建新楼​​:

      • 小医院(<64病房):新容量 = 旧容量*2 + 2
      • 大医院:新容量 = 旧容量*1.5
    • ​搬病人​​:工队把病人搬到新病房楼(System.arraycopy

  5. ​VIP病房分配术​​:

java
Copy
private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1; // 找父病房
        Object e = array[parent];
        if (key.compareTo((T) e) >= 0) break; // 病情不如父?停止
        array[k] = e; // 父病人移到当前病房
        k = parent;   // 当前病房设为父病房
    }
    array[k] = key; // 找到最终位置
}
  • 新病人先住进末尾病房(k = size

  • ​向上冒泡比较​​:

    • 找父病房:父索引 = (k-1)/2
    • 比较病情(优先级):如果比父病人严重,就交换位置
    • 重复直到找到合适位置
  1. 更新病人数+1,院长广播通知医生(notEmpty.signal()
  2. 院长离场解锁(lock.unlock()

💡 ​​示例​​:已有病人[3(轻伤),5(中伤),8(重伤)],新来优先级2的病人:

  • 先放末尾:[3,5,8,2]
  • 2 vs 父(8):2>8? 交换 → [3,5,2,8]
  • 2 vs 新父(3):2>3? 交换 → [2,5,3,8]
  • 形成新小顶堆 → 最紧急的2在堆顶

🎬 场景二:医生救治病人(poll/take操作)

java
Copy
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly(); // 1. 医生申请院长钥匙(可中断)
    E result;
    try {
        // 2. 循环等待直到有病人
        while ((result = dequeue()) == null)
            notEmpty.await(); // 3. 没病人?听广播等待
    } finally {
        lock.unlock(); // 4. 归还钥匙
    }
    return result;
}

private E dequeue() {
    int n = size - 1;
    if (n < 0) return null; // 没病人
    
    Object[] array = queue;
    E result = (E) array[0]; // 1. 取1号病房病人(最紧急)
    E x = (E) array[n];     // 2. 取最后一个病人
    array[n] = null;        // 3. 清空末尾病房
    
    // 4. 堆结构调整
    Comparator<? super E> cmp = comparator;
    if (cmp == null)
        siftDownComparable(0, x, array, n);
    else
        siftDownUsingComparator(0, x, array, n, cmp);
    size = n; // 5. 更新病人数
    return result;
}

// 堆结构下沉调整
private static <T> void siftDownComparable(int k, T x, Object[] array, int n) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    int half = n >>> 1; // 最后一个非叶子节点
    
    while (k < half) {
        int child = (k << 1) + 1; // 左子病房
        Object c = array[child];
        int right = child + 1;
        
        // 选更紧急的子病人
        if (right < n && ((Comparable<T>) c).compareTo((T) array[right]) > 0)
            c = array[child = right];
        
        if (key.compareTo((T) c) <= 0) break; // 比子病人都急?停止
        array[k] = c; // 子病人上移
        k = child;    // 下移到子病房
    }
    array[k] = key; // 放置到最终位置
}

​👨⚕️ 故事细节:​

  1. 李医生申请院长钥匙(lock.lockInterruptibly()

  2. ​核心救治操作​​:

    • 取1号病房病人(堆顶,最紧急)
    • 把最后一个病人移到临时位置
    • ​堆结构调整术​​:
java
Copy
// 示例:取走堆顶[2]后,把末尾8放到堆顶
原始堆: [2, 5, 3, 8] → 取走28放堆顶 → [8,5,3]
// 调整过程:
1. 8 vs 子节点(5,3): 选更紧急的3
2. 8>3? 交换 → [3,5,8]
3. 3 vs 子节点(8): 3<8? 停止
最终堆: [3,5,8]
  • ​向下筛选三步骤​​:

    1. 找左右子病房:左子=2*k+1右子=2*k+2
    2. 选更紧急(优先级更小)的子病人
    3. 如果当前病人不如子病人紧急,就交换位置
    4. 重复直到合适位置
  1. 如果没病人(dequeue返回null),医生听广播等待(notEmpty.await()
  2. 医生归还钥匙(lock.unlock()

⚕️ 核心技术揭秘

1. 堆数据结构(Heap)

java
Copy
// 父子节点关系
parent(i) = (i-1)/2
leftChild(i) = 2*i + 1
rightChild(i) = 2*i + 2

// 堆特性
array[parent(i)] <= array[i] // 小顶堆
  • ​完全二叉树​​:病房严格从左到右排列

  • ​堆序性​​:父病房的病人一定比子病房的紧急(优先级更高)

  • ​高效操作​​:

    • 插入:O(log n)(最坏从叶到根)
    • 取最小:O(1)
    • 删除最小:O(log n)(最坏从根到叶)

2. 动态扩容策略

场景扩容公式示例(旧容量→新容量)
小医院(<64)old + old + 210 → 22
大医院old * 1.5100 → 150
极限情况Integer.MAX_VALUE - 8接近int最大值

3. 并发控制三要素

  1. ​全局锁(单锁)​​:final ReentrantLock lock

    • 插入/删除/扩容时独占访问
  2. ​CAS扩容许可​​:allocationSpinLock

    • Unsafe.compareAndSwapInt确保只有一个线程扩容
  3. ​条件通知​​:final Condition notEmpty

    • 只在​​插入元素后队列非空​​时通知

4. 堆调整算法

java
Copy
// 上浮(插入时)
while (有父节点 && 当前<父节点) {
    交换(当前, 父节点);
    当前位置 = 父位置;
}

// 下沉(删除时)
while (当前位置<非叶节点) {
    找最紧急的子节点;
    if (当前<=子节点) break;
    交换(当前, 子节点);
    当前位置 = 子位置;
}

📊 PBQ 与其他队列对比

特性PriorityBlockingQueue (智能医院)ArrayBlockingQueue (奶茶店)LinkedBlockingQueue (快递中心)
​数据结构​​动态扩容堆​固定循环数组链表
​顺序​​按优先级​FIFOFIFO
​锁机制​​单全局锁​单锁双锁(put/take分离)
​扩容​✅ 1.5倍动态扩容❌ 固定✅ 节点无限扩展
​时间复杂度​插入/删除: O(log n)插入/删除: O(1)插入/删除: O(1)
​内存占用​数组+堆结构连续数组每个节点额外24字节
​最佳场景​任务调度、实时系统固定缓冲区高并发生产消费

💉 智能急诊医院特色服务

1. 定制化病情评估(Comparator)

java
Copy
// 创建VIP优先的医院(数值越小越优先)
PriorityBlockingQueue<Patient> hospital = 
    new PriorityBlockingQueue<>(11, (p1, p2) -> {
        // 优先抢救心脏病人
        if(p1.isHeartAttack() && !p2.isHeartAttack()) return -1;
        if(!p1.isHeartAttack() && p2.isHeartAttack()) return 1;
        
        // 其次看病情等级
        return Integer.compare(p1.getLevel(), p2.getLevel());
    });

2. 批量接诊(drainTo)

java
Copy
// 把病情>=4的病人转到ICU
List<Patient> icuList = new ArrayList<>();
hospital.drainTo(icuList, 10, p -> p.getLevel() >= 4);

3. 扩容时仍可救治(精妙设计)

java
Copy
public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return dequeue(); // 即使正在扩容,也能取数据!
    } finally {
        lock.unlock();
    }
}
  • ​扩容期间​​:其他线程仍可执行出队操作
  • ​设计关键​​:tryGrow()中的lock.unlock()释放锁

💡 核心要点总结

  1. ​堆是心脏​​:数组实现的二叉堆保证O(1)取最小元素

  2. ​动态扩容是生存之道​​:

    • 小容量:翻倍+2
    • 大容量:1.5倍
    • CAS控制单线程扩容
  3. ​全局锁是院长​​:单锁保证线程安全,简化设计

  4. ​堆调整是核心算法​​:

    • ​上浮(siftUp)​​:新病人从底向上比较
    • ​下沉(siftDown)​​:堆顶空缺后从顶向下筛选
  5. ​无界队列​​:理论上可扩容到Integer.MAX_VALUE

  6. ​优先级为王​​:不是FIFO,而是按Comparator或自然顺序

下次使用PriorityBlockingQueue时,就想象这家智能急诊医院:院长(lock)掌控全局,护士用堆结构(siftUp)安排病房,医生按堆顶顺序救治(siftDown),工队动态扩建病房(tryGrow),而急救广播(notEmpty)确保没有病人被遗漏。这才是处理优先级任务的终极解决方案!🚑🏆