手写线程池与锁

共 19 题
📑 题目列表 19 题
#
★★★

1. 手写线程池,任务队列、Worker 循环、拒绝策略、优雅关闭如何实现?

请手写一个线程池,说明任务队列、Worker 循环、拒绝策略与优雅关闭如何实现?

  • 核心线程与任务队列
  • Worker 循环取任务执行
  • 拒绝策略与优雅关闭

线程池核心是:维护一个任务队列(BlockingQueue)和一组 Worker 线程。提交任务时若线程数 < corePoolSize 则新建 Worker 立即执行;否则放入队列;队列满且线程数 < maxPoolSize 则新建 Worker;仍满则触发拒绝策略。Worker 循环从队列 take 任务执行,若无任务则阻塞等待。拒绝策略:AbortPolicy(抛异常)、CallerRunsPolicy(调用者执行)、DiscardPolicy(丢弃)、DiscardOldestPolicy(丢弃最旧)。优雅关闭:shutdown 不再接受新任务并等待已提交任务完成,shutdownNow 中断并返回未完成任务;需等待所有 Worker 退出并清空队列。

线程池通过"核心线程 + 队列 + 非核心线程"分层应对突发流量,拒绝策略兜底。优雅关闭的关键是区分"新任务"与"已提交任务",并让 Worker 在队列空后退出而不是一直阻塞。

public class SimpleThreadPool {
    private final BlockingQueue<Runnable> queue;
    private final List<Worker> workers = new ArrayList<>();
    private volatile boolean running = true;
    public SimpleThreadPool(int coreThreads, int cap){
        queue = new ArrayBlockingQueue<>(cap);
        for (int i=0;i<coreThreads;i++){ Worker w=new Worker(); workers.add(w); w.start(); }
    }
    public void execute(Runnable task) {
        if (!running) throw new RejectedExecutionException("已关闭");
        queue.offer(task); // 满则拒绝
    }
    public void shutdown() {
        running = false;
        for (Worker w : workers) w.interrupt(); // 中断等待
    }
    class Worker extends Thread {
        public void run() {
            while (running || !queue.isEmpty()) {
                try {
                    Runnable task = queue.poll(100, TimeUnit.MILLISECONDS);
                    if (task != null) task.run();
                } catch (InterruptedException e) { /* 退出 */ }
            }
        }
    }
}
#
★★★

2. 手写一个可重入锁(基于 synchronized 或 AQS 思路)并说明重入计数的作用?

请手写一个可重入锁,基于 synchronized 或 AQS 思路,并说明重入计数的作用?

  • 可重入的语义
  • 持有者与重入计数
  • 与 AQS 的关系

可重入锁允许同一线程多次获取同一把锁而不死锁。实现上需要记录"当前持有锁的线程"和"重入计数":获取锁时若无人持有则当前线程持有且计数=1;若持有者就是当前线程则计数+1;释放时计数-1,减到 0 才真正释放锁并唤醒等待者。synchronized 本身可重入,ReentrantLock 基于 AQS 的 state 字段记录重入计数(持有者线程每次获取 state+1,释放 state-1)。重入计数的作用:区分"该释放锁了"与"只是减少一次重入",避免嵌套调用时提前释放导致其他线程进入临界区。

没有重入计数,同一线程嵌套加锁会死锁(第二次获取时发现锁被占用而阻塞等待自己)。计数让锁的获取/释放可配对嵌套,是"可重入"的实现基础。

public class ReentrantLockSimple {
    private Thread owner;
    private int count;
    public synchronized void lock() {
        Thread cur = Thread.currentThread();
        if (owner == null) { owner = cur; count = 1; }
        else if (owner == cur) count++; // 重入
        else { while (owner != null) { try { wait(); } catch (InterruptedException e){} } owner=cur; count=1; }
    }
    public synchronized void unlock() {
        if (--count == 0) { owner = null; notifyAll(); }
    }
}
#
★★★

3. ThreadLocal 的实现原理,ThreadLocalMap 的线性探测与弱引用 Entry 如何导致内存泄漏,如何正确清理?

请说明 ThreadLocal 的实现原理,ThreadLocalMap 的线性探测与弱引用 Entry 如何导致内存泄漏,以及如何正确清理?

  • ThreadLocalMap 结构
  • 线性探测解决哈希冲突
  • 弱引用 key 与内存泄漏

每个 Thread 内部有 ThreadLocalMap,key 是 ThreadLocal 本身(弱引用),value 是线程本地值。ThreadLocalMap 用开放地址法(线性探测)解决哈希冲突,查找时沿数组向后探测直到找到空槽。内存泄漏:Entry 的 key 是弱引用,当 ThreadLocal 外部强引用消失后,key 会被 GC 回收变为 null,但 value 仍被强引用(Entry 里 key=null 但 value 存在),若线程是线程池里的长生命周期线程,这条 Entry 永远无法被回收,value 就泄漏了。正确清理:用完调用 remove() 删除该 Entry;ThreadLocalMap 在 get/set 时也会清理 key 为 null 的过期 Entry(惰性清理),但最可靠的是显式 remove。

弱引用 key 是为了让 ThreadLocal 不阻止 GC,但 value 的强引用残留导致泄漏。解决靠 remove 显式清理 + 结构内的惰性清理。线程池中线程复用,泄漏尤为严重。

ThreadLocal<Integer> tl = new ThreadLocal<>();
try {
    tl.set(1);
    int v = tl.get();
} finally {
    tl.remove(); // 显式清理,防止内存泄漏
}
#
★★★

4. 手写 CountDownLatch 与 CyclicBarrier,基于 AQS 共享锁与栅栏重置(Generation)的核心差异如何?

请手写 CountDownLatch 与 CyclicBarrier,说明基于 AQS 共享锁与栅栏重置(Generation)的核心差异?

  • CountDownLatch 的一次性计数
  • CyclicBarrier 的可重置
  • 基于 AQS 共享锁 vs 基于锁+条件

CountDownLatch 基于 AQS 共享锁,state 初始为计数 N,countDown 使 state 减 1(CAS),state 归零时唤醒所有等待线程;它是一次性的,不可重置。CyclicBarrier 用 ReentrantLock + Condition 实现,parties 个线程都到达(await)后一起放行,通过 Generation 表示"一代",新一代重置计数;它可重复使用(reset 或自动进入下一代)。核心差异:CountDownLatch 是"一锤定音"(等 N 个事件后一次性放行),CyclicBarrier 是"循环栅栏"(每凑齐 N 个线程就放行一轮,可重置)。CountDownLatch 用 AQS 共享锁,CyclicBarrier 用 lock+condition 并维护 generation。

两者都实现"多线程等待",但语义不同:CountDownLatch 关注"事件到达次数",CyclicBarrier 关注"线程到达齐整"。CyclicBarrier 的 Generation 在 barrier 被打破或重置时递增,用于区分新旧一轮。

// CountDownLatch 简化:AQS 共享锁思想
public class CountDownLatchSimple {
    private int count;
    public CountDownLatchSimple(int n){ count = n; }
    public synchronized void countDown(){ if (--count == 0) notifyAll(); }
    public synchronized void await() throws InterruptedException {
        while (count > 0) wait();
    }
}
// CyclicBarrier 简化:可重置
public class CyclicBarrierSimple {
    private final int parties;
    private int count;
    private int generation;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition cond = lock.newCondition();
    public CyclicBarrierSimple(int n){ parties = n; count = n; }
    public void await() throws InterruptedException {
        lock.lock();
        try {
            int myGen = generation;
            if (--count == 0) { generation++; count = parties; cond.signalAll(); } // 新一代
            else while (myGen == generation) cond.await();
        } finally { lock.unlock(); }
    }
}
#
★★★

5. 手写 AQS 简化版,CLH 等待队列、独占/共享模式的 acquire/release

请手写 AQS 简化版,说明 CLH 等待队列、独占/共享模式的 acquire/release 实现?

  • CLH 队列的构建与阻塞
  • 独占模式 acquire/release
  • 共享模式 acquireShared/releaseShared

AQS 用 volatile int state 表示资源状态,加一个 CLH 变体队列存储等待线程。独占模式 acquire:先尝试 CAS 修改 state,失败则把当前线程封装成 Node 入队,然后循环检查前驱是否为头节点且能获取资源,若能则出队,否则 LockSupport.park 阻塞;release 时 CAS 释放 state 并唤醒头节点的后继。共享模式 acquireShared:尝试获取,若成功则传播唤醒后继(setHeadAndPropagate),失败则入队阻塞;releaseShared 释放后唤醒后继。CLH 队列通过前驱节点链保证先来先服务的公平性(可选)。

AQS 的核心是"用 state 表示资源 + 用队列管理等待者"。acquire 是"自旋检查前驱 + 阻塞",release 是"释放 + 唤醒"。共享模式多一个"传播"机制,让多个合法等待者同时被唤醒。

public abstract class AbstractQueuedSynchronizerSimple {
    volatile int state;
    final class Node { Thread thread; Node prev, next; }
    volatile Node head = new Node(), tail = head;
    protected abstract boolean tryAcquire(int arg);
    protected abstract boolean tryRelease(int arg);
    public void acquire(int arg) {
        if (!tryAcquire(arg)) {
            Node n = new Node(); n.thread = Thread.currentThread();
            // 简化:入队 tail
            synchronized (this) { tail.next = n; n.prev = tail; tail = n; }
            for (;;) {
                if (n.prev == head && tryAcquire(arg)) { head = n; return; }
                LockSupport.park();
            }
        }
    }
    public void release(int arg) {
        if (tryRelease(arg)) {
            Node next = head.next;
            if (next != null) LockSupport.unpark(next.thread);
        }
    }
}
#
★★

6. 手写读写锁,读者优先与写者优先策略的实现差异如何?

请手写读写锁,说明读者优先与写者优先策略的实现差异?

  • 读写锁的状态维护
  • 读者优先与写者优先
  • 饥饿问题

读写锁允许多个读者并发,但写者独占。读者优先:只要有读者在,新读者就能进入,写者可能被源源不断的读者饿死;实现简单,读者到来时若无人写则直接放行。写者优先:新读者到来时若已有写者等待,则读者也等待,让写者优先获得机会,避免写者饥饿;实现上需要额外记录"等待写者数",读者进入前检查是否有写者等待。写者优先能避免写者饥饿,但读者可能被阻塞。也可用公平模式(FIFO 队列)让读者写者按到达顺序排队。

差异核心是"新读者到来时是否检查等待中的写者"。读者优先简单但写者可能饿死;写者优先保证写者不被饿死但牺牲读者吞吐。生产环境常用公平锁或写者优先。

public class ReadWriteLockPriority {
    private int readers = 0, writers = 0, waitingWriters = 0;
    public synchronized void readLock() throws InterruptedException {
        while (writers > 0 || waitingWriters > 0) wait(); // 写者优先:有写者等待则读者等待
        readers++;
    }
    public synchronized void readUnlock(){ readers--; if (readers==0) notifyAll(); }
    public synchronized void writeLock() throws InterruptedException {
        waitingWriters++;
        try { while (readers>0 || writers>0) wait(); writers++; }
        finally { waitingWriters--; }
    }
    public synchronized void writeUnlock(){ writers--; notifyAll(); }
}
#
★★

7. 手写一个限流器(令牌桶/滑动窗口)并对比两种算法的内存与精度?

请手写一个限流器,实现令牌桶或滑动窗口,并对比两种算法的内存与精度?

  • 令牌桶的令牌生成与消费
  • 滑动窗口的计数与滑动
  • 内存与精度的权衡

令牌桶:桶内容量有限,按固定速率持续补充令牌,请求来时消耗一个令牌,无令牌则拒绝或等待。突发可用桶内积攒的令牌,允许一定突发。实现用一个计数器记录当前令牌数 + 上次补充时间按速率推算。滑动窗口:把时间分成固定窗口,记录每个窗口(或滑动窗口内所有请求)的计数,窗口内请求数超限则拒绝。可以是固定窗口(简单但边界突刺)或滑动窗口(精确反映单位时间请求数)。内存与精度:令牌桶只记录"令牌数+时间"两个变量,内存极小,控制的是"平均速率+突发容量";滑动窗口需要记录时间窗口内的请求,内存与窗口/桶数相关,精度更高(能精确限制每个时间段的请求数)但内存更大。令牌桶适合控制速率与允许突发,滑动窗口适合精确限流。

令牌桶是"速率+突发"模型,内存 O(1);滑动窗口是"精确计数"模型,内存与粒度相关。生产常用令牌桶(如 Guava RateLimiter)兼顾突发与控制,严格限流用滑动窗口。

public class TokenBucket {
    private final int capacity;
    private final double rate; // 每秒补充令牌数
    private double tokens;
    private long lastRefill;
    public TokenBucket(int cap, double rate){ capacity=cap; this.rate=rate; tokens=cap; lastRefill=System.currentTimeMillis(); }
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        tokens = Math.min(capacity, tokens + (now-lastRefill)/1000.0 * rate);
        lastRefill = now;
        if (tokens >= 1) { tokens -= 1; return true; }
        return false;
    }
}
#
★★

8. 手写 Semaphore,AQS 共享计数的 acquire/release 与公平/非公平模式的实现差异如何?

请手写 Semaphore,说明 AQS 共享计数的 acquire/release 与公平/非公平模式的实现差异?

  • AQS 共享计数
  • acquire 递减许可、release 递增许可
  • 公平与非公平

Semaphore 用 AQS 的 state 表示剩余许可数。acquire 时若 state >= 需要的许可数则 CAS 递减并成功,否则入队等待;release 时 CAS 递增 state 并唤醒等待者。非公平模式 permit 直接尝试 CAS 递减,谁抢到谁用,可能让刚到的线程插队;公平模式先检查队列中是否有等待者,有则必须排队(hasQueuedPredecessors),保证先到先得。非公平吞吐更高,公平避免饥饿但可能降低吞吐。

非公平是"抢到就是赢",公平是"严格按队列顺序"。两者差别只在 acquire 开始时是否检查队首等待者。Semaphore 是共享锁,多个线程可同时获得多个许可。

public class SemaphoreSimple {
    private int permits;
    public SemaphoreSimple(int n){ permits = n; }
    public synchronized void acquire() throws InterruptedException {
        while (permits == 0) wait();
        permits--;
    }
    public synchronized void release(){ permits++; notify(); }
}
#
★★

9. 手写工作窃取(Work-Stealing)线程池或 ForkJoinPool 的分治模型,窃取为什么从队尾?

请手写工作窃取(Work-Stealing)线程池或 ForkJoinPool 的分治模型,说明窃取为什么从队尾?

  • 每个线程双端队列
  • 窃取从队尾
  • 分治任务模型

ForkJoinPool 每个工作线程有自己的双端队列(WorkQueue),执行任务时产生的新子任务 push 到队尾(LIFO),线程自己从队尾取任务执行;当自己队列空时,从其他线程的队尾窃取任务(steal)。窃取从队尾的原因:线程自己从队尾取(LIFO,任务更可能是刚生成的、粒度更小的子任务),窃取者从队尾取,与拥有者竞争队尾,但通过 CAS 保证安全;更重要的是,窃取从队尾能让窃取者拿到"大任务"(靠近队头的大块任务还没被拆分),而被窃取线程继续处理队尾的小任务,减少窃取中的竞争。实际上窃取从队尾取的是队尾,而拥有者也从队尾取,两者都取队尾,配合 CAS 保证安全。分治模型:任务递归拆分成子任务(fork),子任务结果合并(join)。

工作窃取负载均衡的关键是"窃取者从队尾取任务",这样与拥有者(从队尾取)竞争局部;同时窃取者偏向取大任务,减小窃取频率。与简单"一个共享队列"相比,工作窃取避免全局锁竞争。

// 工作窃取思路:每个线程一个 Deque
public class WorkStealing {
    final Deque<Runnable>[] deques;
    final int nThreads;
    public WorkStealing(int n){ nThreads=n; deques=new Deque[n]; for(int i=0;i<n;i++) deques[i]=new ArrayDeque<>(); }
    public void submit(int tid, Runnable task){ deques[tid].push(task); } // 推入队尾
    public Runnable steal(int tid){
        for (int i=0;i<nThreads;i++){
            int other = (tid+i)%nThreads;
            Runnable t = deques[other].pollLast(); // 从队尾窃取
            if (t!=null) return t;
        }
        return null;
    }
}
#
★★

10. 手写支持超时与中断的锁,LockSupport.parkNanos 与 wait/notify 相比的可靠性差异如何?

请手写支持超时与中断的锁,说明 LockSupport.parkNanos 与 wait/notify 相比的可靠性差异?

  • parkNanos 超时与中断
  • wait/notify 的局限
  • 信号丢失与中断处理

LockSupport.parkNanos(long) 让线程阻塞给定的纳秒数,超时或被 unpark 后返回,且响应中断(抛出 InterruptedException 或返回)。相比 wait/notify:wait 必须持有 synchronized 锁且必须在循环中检查条件(防止虚假唤醒与信号丢失),notify 需要与 wait 在同一把锁上;LockSupport 不依赖锁,unpark 相当于"发信号",且 unpark 可以提前到 park 之前(unpark 有计数),不会丢失信号。可靠性上,LockSupport 更灵活可靠,不依赖锁的监视器,能在超时与中断两个维度精确控制。wait/notify 若未在循环中检查条件,可能因虚假唤醒或信号早到而丢失唤醒。

wait/notify 的"信号丢失"问题在于:notify 若发生在 wait 之前,该信号就丢了;必须在循环中检查条件。LockSupport 的 unpark 有许可计数,即使先 unpark 后 park 也能立即返回,避免丢失。

public class TimedLock {
    private final AtomicBoolean locked = new AtomicBoolean();
    public boolean tryLock(long timeoutNanos) throws InterruptedException {
        long deadline = System.nanoTime() + timeoutNanos;
        while (!locked.compareAndSet(false, true)) {
            long remaining = deadline - System.nanoTime();
            if (remaining <= 0) return false;
            LockSupport.parkNanos(remaining); // 超时自动返回
            if (Thread.interrupted()) throw new InterruptedException(); // 响应中断
        }
        return true;
    }
    public void unlock(){ locked.set(false); } // 无等待者队列,等待者靠 parkNanos 超时后重试 CAS;若需真正唤醒应维护等待者队列
}
#
★★

11. 手写线程池时如何埋点监控(任务数、队列深度、拒绝数)与告警

手写线程池时如何埋点监控(任务数、队列深度、拒绝数)与告警?

  • 监控指标:活跃线程、队列深度、拒绝数
  • 埋点与指标采集
  • 告警阈值与触发

监控指标包括:线程池大小(core/max)、活跃线程数、任务队列深度、已完成任务数、被拒绝任务数、任务执行耗时分布。埋点方式:在 execute 前置计数、Worker 执行前后记录耗时、拒绝策略处累计拒绝数;可用计数器(LongAdder)或引入 Micrometer/指标库暴露。告警:当队列深度超过阈值、活跃线程达到 max、拒绝数持续增长、任务积压时间超限时触发告警(日志/监控平台/钉钉)。可提供 getActiveCount、getQueue().size()、getTaskCount() 等方法供外部采集。

线程池监控是为了发现"线程池资源耗尽"的隐患:队列堆积说明处理速度跟不上,拒绝说明资源彻底枯竭。指标采集要线程安全(LongAdder 计数),告警结合阈值与趋势。

public class MonitoredThreadPool {
    private final LongAdder rejected = new LongAdder();
    private final BlockingQueue<Runnable> queue;
    public void execute(Runnable task) {
        if (!queue.offer(task)) { rejected.increment(); alert(); } // 队列满拒绝并告警
    }
    public long queueDepth() { return queue.size(); }
    public long rejectedCount() { return rejected.sum(); }
    private void alert(){ /* 超过阈值告警 */ }
}
#
★★

12. 手写线程池时如何支持"核心线程超时回收"(allowCoreThreadTimeOut)与动态调整 corePoolSize,空闲线程的 keepAlive 如何实现?

手写线程池时如何支持"核心线程超时回收"(allowCoreThreadTimeOut)与动态调整 corePoolSize?空闲线程的 keepAlive 如何实现?

  • allowCoreThreadTimeOut 语义
  • keepAlive 的定时等待实现
  • 动态调整核心线程数

keepAlive 用 poll(timeout) 代替 take():当线程数量超过 corePoolSize 时,Worker 用 poll(keepAliveTime) 从队列取任务,超时未取到任务则回收该线程。allowCoreThreadTimeOut=true 时,核心线程也会在空闲 keepAlive 时间后被回收,此时线程池可能缩到 0 个线程。动态调整 corePoolSize 用 setCorePoolSize:减小 core 时,多余的空闲核心线程会被中断回收;增大时新任务会触发新建核心线程。实现上,Worker 循环里每次取任务根据"是否允许核心线程超时"选择 take() 或 poll(keepAliveTime)。

核心是"用 poll 带超时替代 take 阻塞"。allowCoreThreadTimeOut 让核心线程也能空闲回收,适合突发流量后的资源释放。动态调整则改变新建线程的阈值。

public class KeepAliveWorker extends Thread {
    // allowCoreThreadTimeOut 与 alive:
    // 取任务:若允许核心超时,用 poll(keepAlive);否则额外线程用 poll,核心线程用 take
    Runnable task = allowCoreTimeout || isExtra
        ? queue.poll(keepAliveNanos, NANOSECONDS)
        : queue.take();
    if (task == null) { /* 超时,回收线程 */ return; }
}
#
★★

13. 手写一个异步任务编排器(依赖 DAG),任务间的依赖、失败传播与超时如何实现,与 CompletableFuture 的 thenCombine/allOf 对应关系如何?

请手写一个异步任务编排器(依赖 DAG),说明任务间的依赖、失败传播与超时如何实现,以及与 CompletableFuture 的 thenCombine/allOf 的对应关系?

  • DAG 依赖构建与拓扑执行
  • 失败传播与超时
  • 与 CompletableFuture 的对应

任务编排器用 DAG 表达任务依赖:每个任务有前置任务集合,前置全部完成后任务就绪;可用拓扑排序或基于"前置完成计数"调度,任务完成后递减后继的就绪计数,归零则提交执行。失败传播:任务失败时把异常记录并取消/标记所有依赖它的后继。超时:每个任务可设超时,超时则标记失败并取消。对应关系:等两个任务都完成再合并 = thenCombine / allOf;前置完成后的回调 = thenApply/thenAccept;异常处理 = exceptionally/handle;多步编排 = thenCompose 组合。

编排器本质是"依赖图 + 异步执行"。CompletableFuture 提供了成熟的 thenXXX/allOf/anyOf 组合能力,手写编排器是在更底层实现同样的依赖调度。失败与超时都要沿依赖链传播。

// 简化:任务就绪计数
public class TaskNode {
    final Runnable action;
    final List<TaskNode> deps = new ArrayList<>();
    final List<TaskNode> dependents = new ArrayList<>();
    int readyCount;
    public void runAndNotify() {
        try { action.run(); } catch (Exception e) { /* 失败传播 */ }
        for (TaskNode d : dependents) if (--d.readyCount == 0) schedule(d);
    }
}
// 对应 CompletableFuture
CompletableFuture.allOf(f1, f2).thenApply(...); // 都完成后再合并
#
★★

14. 手写线程池的拒绝策略扩展,如何实现"重试入队""降级到其他池""优雅降级"三种自定义策略,各自适用什么场景?

请手写线程池的拒绝策略扩展,说明如何实现"重试入队""降级到其他池""优雅降级"三种自定义策略及其适用场景?

  • 拒绝策略接口
  • 重试入队、降级池、优雅降级
  • 场景匹配

自定义拒绝策略实现 RejectedExecutionHandler 的 rejectedExecution 方法。重试入队:在 rejected 时短暂等待后再次 offer,或把任务放入备用队列,适合"瞬时峰值、不愿丢任务"的场景。降级到其他池:把任务提交给另一个备用线程池(如低优先级池或更宽的池),适合"主池满但可用副池"的场景。优雅降级:拒绝时降级为简化处理(如日志、丢弃部分、用旧结果、异步补记),保证核心功能不崩溃,适合"高峰可接受部分损失"的场景。三种策略都可在 rejected 方法里灵活实现,是所有线程池共用。

拒绝策略是线程池的兜底。重试适用于"可等",降级池适用于"有备用资源",优雅降级适用于"可降质量"。选择取决于业务对"任务丢失"的容忍度。

public class RetryPolicy implements RejectedExecutionHandler {
    private final BlockingQueue<Runnable> backup;
    public RetryPolicy(BlockingQueue<Runnable> b){ backup = b; }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        try { backup.offer(r, 100, TimeUnit.MILLISECONDS); } // 重试入队到备用队列
        catch (InterruptedException ex) { Thread.currentThread().interrupt(); }
    }
}
#

15. 手写"生产者-消费者",如何避免忙等与信号丢失?

请手写"生产者-消费者"模式,说明如何避免忙等与信号丢失?

  • 阻塞队列实现
  • 忙等与信号丢失
  • 条件等待与循环检查

生产者-消费者用共享的阻塞队列解耦:生产者 put 入队,消费者 take 出队。避免忙等:用条件等待(wait/await 或 take 阻塞)让线程在队列空/满时休眠,而不是自旋轮询,减少 CPU 浪费。避免信号丢失:等待必须用 while 循环检查条件(notify 只唤醒一个,若消费者都醒来发现队列空又睡着,可能漏掉后续信号;且 notify 可能丢失),并在循环中检查;或使用 take 的阻塞语义由队列内部保证。生产者-消费者还可加标志位区分"是否还有任务"。

忙等浪费 CPU;信号丢失发生在"notify 先于 wait"或"多个等待者被一个 notify 唤醒后条件仍不满足"。while 循环检查 + 条件变量的正确使用是避免两类问题的关键。

public class ProducerConsumer {
    private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(100);
    public void producer() throws InterruptedException {
        for (int i=0;i<1000;i++) queue.put(i); // 满则阻塞,不忙等
    }
    public void consumer() throws InterruptedException {
        while (true) {
            int v = queue.take(); // 空则阻塞
            process(v);
        }
    }
    void process(int v){}
}
#

16. 手写 FutureTask 简化版,状态机(NEW/RUNNING/DONE/CANCELLED)与等待唤醒如何实现?

请手写 FutureTask 简化版,说明状态机(NEW/RUNNING/DONE/CANCELLED)与等待唤醒如何实现?

  • 状态机流转
  • 结果等待与唤醒
  • 取消与中断

FutureTask 用状态机管理:NEW(新建) → RUNNING(运行中) → DONE(完成) 或 CANCELLED(取消)。run() 时若状态为 NEW 则置为 RUNNING 并执行 Callable,结果存入 outcome 并置为 DONE,然后唤醒所有等待者;get() 在状态非 DONE 时阻塞等待,完成后返回结果或抛出异常;cancel() 若状态为 NEW 则置为 CANCELLED 并可能中断执行线程。等待唤醒可用锁 + 条件或 wait/notify;状态用 volatile 保证可见性。

状态机保证"只执行一次、结果只存一次、取消与执行互斥"。get 的等待唤醒与普通 Condition 类似,关键是状态置 DONE 后必须唤醒等待者,避免 get 永久阻塞。

public class FutureTaskSimple<V> {
    private volatile int state; // 0 NEW,1 RUNNING,2 DONE,3 CANCELLED
    private final Callable<V> callable;
    private V result;
    private Exception error;
    private final Object lock = new Object();
    public FutureTaskSimple(Callable<V> c){ callable=c; }
    public void run() {
        if (state != 0) return;
        state = 1;
        try { result = callable.call(); }
        catch (Exception e) { error = e; }
        finally { state = 2; synchronized(lock){ lock.notifyAll(); } }
    }
    public V get() throws Exception {
        synchronized(lock) {
            while (state != 2 && state != 3) lock.wait();
        }
        if (error != null) throw error;
        return result;
    }
    public boolean cancel(){ if (state==0){ state=3; synchronized(lock){lock.notifyAll();} return true;} return false; }
}
#

17. 手写读写锁的升级/降级边界,为什么读锁不能升级为写锁,写锁降级为何安全?

手写读写锁时说明升级/降级边界:为什么读锁不能升级为写锁,写锁降级为何安全?

  • 读锁升级写锁的死锁风险
  • 写锁降级为读锁的安全性
  • 锁升级的判定

读锁不能升级为写锁:多个线程都持有读锁,若都尝试升级为写锁,会互相等待对方释放读锁,造成死锁(每个线程都等别人降级,谁都等不到)。写锁降级为读锁是安全的:写锁是独占的,持有者只有一个线程,它降级为读锁时没有其他线程竞争,不会死锁。降级指持有写锁时获取读锁再释放写锁,这样保持读锁继续读,避免直接释放写锁后写者插队。读锁升级写锁在支持时需实现者保证无死锁(如升级时检查是否排他),ReentrantReadWriteLock 不支持读锁升级。

死锁源于"多线程同时持有读锁并都想升级"。降级之所以安全,是因为写锁独占,只有持有者能降级,锁的持有者单一,无竞争循环等待。

ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
// 写锁降级为读锁(安全)
rw.writeLock().lock();
try {
    // 写操作
    rw.readLock().lock(); // 先获取读锁
    rw.writeLock().unlock(); // 再释放写锁,降级为读锁
    // 继续以读锁读
} finally { rw.readLock().unlock(); }
// 读锁升级为写锁:ReentrantReadWriteLock 不支持,会死锁
#

18. 手写 Ticket Lock(公平自旋锁)与自旋锁在临界区极短场景的应用

请手写 Ticket Lock(公平自旋锁)与自旋锁,说明在临界区极短场景的应用?

  • Ticket Lock 的公平性
  • 自旋锁的实现
  • 临界区极短场景

Ticket Lock 用两个原子计数器:取号(ticket)和叫号(now serving)。线程获取时取号并自旋等待轮到自己的号,释放时递增 serving。它保证公平(先取号先获得)。自旋锁(如 CAS 实现的 spinlock)在临界区极短时,线程自旋等待比阻塞切换更高效,因为上下切换开销大,而临界区很短时自旋很快结束。适合多核下临界区极短、锁竞争不激烈的场景。但自旋会在持锁线程被调度走时浪费 CPU(锁持有者被抢占),且单核无用。

临界区极短时,自旋等待的耗时远小于线程阻塞/唤醒的上下文切换开销,故用自旋。Ticket Lock 在自旋基础上加了公平性,防止饥饿,但每个线程都要轮询自己的号,Cache 一致性流量大。

public class TicketLock {
    private final AtomicInteger ticket = new AtomicInteger();
    private final AtomicInteger serving = new AtomicInteger();
    public int lock() {
        int my = ticket.getAndIncrement();
        while (serving.get() != my) { /* 自旋等待 */ }
        return my;
    }
    public void unlock(int my){ serving.compareAndSet(my, my+1); }
}
#

19. 手写"带超时的阻塞队列",offer(timeout) 与 poll(timeout) 的底层条件等待如何实现,与 ThreadPoolExecutor 的 workQueue 如何配合?

请手写"带超时的阻塞队列",说明 offer(timeout) 与 poll(timeout) 的底层条件等待如何实现,以及与 ThreadPoolExecutor 的 workQueue 如何配合?

  • offer(timeout)/poll(timeout) 的定时等待
  • 条件变量的 awaitNanos
  • 与线程池 workQueue 配合

offer(timeout) 在队列满时用 condition.awaitNanos(timeout) 等待,超时仍未空则返回 false;poll(timeout) 在队列空时用 awaitNanos 等待,超时未取到则返回 null。实现上要循环等待并重新计算剩余时间(awaitNanos 返回剩余纳秒)。ThreadPoolExecutor 中,非核心线程的 keepAlive 就是用 workQueue.poll(keepAliveTime) 实现的:取到任务则执行,超时返回 null 则回收线程;核心线程用 take() 阻塞。所以带超时的阻塞队列是线程池空闲线程回收的基础。

定时条件等待的核心是 "awaitNanos 返回剩余时间,循环再等待"。offer/poll 的超时语义让调用方可以"不无限等待",配合线程池的 keepAlive 实现空闲回收。

public boolean offer(T v, long timeout, TimeUnit unit) throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    lock.lock();
    try {
        while (count == items.length) {
            if (nanos <= 0) return false;
            nanos = notFull.awaitNanos(nanos); // 等待并在剩余时间后返回
        }
        enqueue(v); return true;
    } finally { lock.unlock(); }
}
public T poll(long timeout, TimeUnit unit) throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    lock.lock();
    try {
        while (count == 0) {
            if (nanos <= 0) return null;
            nanos = notEmpty.awaitNanos(nanos);
        }
        return dequeue();
    } finally { lock.unlock(); }
}