最近系统梳理了 Java 并发编程的核心知识点,将一些关键内容整理如下,方便日后回顾。

1. JMM 与 Happens-Before 规则理解

JMM(Java 内存模型)定义了线程如何通过内存进行交互,以及何时能够看到其他线程的写入操作。它主要解决了可见性与有序性问题(注意:原子性不在其范畴内)。

Happens-Before 是 JMM 的核心概念,常见的规则包括:

  1. 程序次序规则:单线程内顺序执行
  2. 监视器锁规则:解锁操作先于后续的加锁操作
  3. volatile 变量规则:写操作先于后续的读操作
  4. 线程启动与终止规则:start() 先于线程内操作,线程内操作先于终止检测
  5. 传递性规则:A → B,B → C,则 A → C
  6. 中断规则:对线程的 interrupt() 调用先于被中断线程的响应
  7. final 字段规则:正确构造的 final 字段在构造完成后对其他线程可见

指令重排序是允许的,但前提是不能破坏 Happens-Before 约束。JIT 编译器的重排序受到内存屏障的限制,synchronized 和 volatile 在编译后都会生成相应的屏障指令。

// 典型示例:volatile 保证可见性
volatile boolean ready = false;
int data = 0;

void writer() {
    data = 42;          // 普通写
    ready = true;       // volatile 写,建立HB关系
}

void reader() {
    if (ready) {        // volatile 读,看到true则之前的写操作都可见
        System.out.println(data); // 保证输出42
    }
}

易错点:
• 误以为 volatile 能保证复合操作的原子性(如 i++)
• 不了解 final 字段的发布规则导致对象逸出

延伸思考:为什么 final 字段构造完成后对其他线程可见?
参考答案:因为 JMM 在构造结束时插入了内存屏障,且要求对象引用在构造完成前不能逸出。

2. volatile 与 synchronized 的区别与适用场景

这两个关键字解决了不同层面的并发问题:
volatile:
• 保证可见性和禁止指令重排序
• 不保证原子性
• 适用场景:状态标志位、一次性发布(如配置加载)

synchronized:
• 保证互斥访问和可见性
• 通过进入/退出临界区时的内存屏障实现
• 适用场景:需要原子性操作的复合逻辑

// volatile 典型用法:停止标志
class StoppableTask {
    private volatile boolean stopRequested;
    
    public void run() {
        while (!stopRequested) {
            // 执行任务
        }
    }
    
    public void stop() {
        stopRequested = true;
    }
}

易错点:使用 volatile 进行计数操作(如 ++)无法保证原子性

延伸思考:AtomicInteger 如何实现原子性?
答:AtomicInteger 通过 CAS + 自旋循环实现原子性,适合计数器场景。

3. CAS 原理与 ABA 问题解决方案

CAS(Compare-And-Swap)是无锁编程的核心操作:
• 比较内存中的值与期望值,相等则写入新值
• 基于硬件指令实现,冲突时通过自旋重试
• JDK9+ 使用 VarHandle 替代 Unsafe

ABA 问题:值从 A → B → A 的变化过程,CAS 无法感知中间状态变化
• 解决方案:添加版本号(AtomicStampedReference)或使用带标记的指针

// 使用版本号解决ABA问题
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);

int[] stampHolder = new int[1];
int currentStamp = ref.getStamp();
int currentValue = ref.get(stampHolder);

// 更新时同时检查值和版本号
ref.compareAndSet(currentValue, 200, currentStamp, currentStamp + 1);

注意:高竞争场景下自旋可能导致 CPU 占用过高,需考虑退避策略

4. synchronized 与 ReentrantLock 选择策略

两者都是可重入互斥锁,但各有特点:
synchronized:
• 语法简洁,JVM 内置支持
• JIT 编译器会进行偏向锁、轻量级锁等优化
• 自动释放锁,不易遗漏

ReentrantLock:基于 AQS(state + CLH 队列)
• 支持公平锁选项
• 支持可中断的锁获取
• 支持超时尝试获取锁
• 支持多个条件变量(Condition)

// ReentrantLock 的典型用法
ReentrantLock lock = new ReentrantLock(true); // 公平锁

try {
    if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
        try {
            // 临界区操作
        } finally {
            lock.unlock(); // 必须手动释放
        }
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

5. AQS 核心设计思想

AQS(AbstractQueuedSynchronizer)是 Java 并发包的核心基础框架:

核心组件:
• state:同步状态,不同子类有不同语义(如重入次数、许可数等)
• CLH 队列:双向队列管理等待线程
• 两种模式:独占模式(ReentrantLock)和共享模式(Semaphore)
条件变量:ConditionObject 维护单独的条件队列,signal 时转移到同步队列

工作流程:

  1. 尝试获取锁(tryAcquire)
  2. 失败则加入队列,park 等待
  3. 释放锁时唤醒后继节点

注意:自定义 AQS 时需要正确处理可重入和中断响应
共享模式的典型实现?(Semaphore、CountDownLatch)

6. Condition 与 wait/notify 对比

Condition 提供了比传统 wait/notify 更精细的线程协调机制:

优势:
• 一个锁可以关联多个条件队列
• 支持精确唤醒(signal vs signalAll)
• 更清晰的语义表达(wait/notify 依赖对象监视器,只有一个等待集)

// 生产者-消费者模式中使用两个Condition
class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();
    
    final Object[] items = new Object[100];
    int putptr, takeptr, count;
    
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }
    // take方法类似
}

注意:必须在持有锁时调用 await/signal,且 signal 可能早于 await 导致信号丢失。
为什么必须在 try/finally 中 unlock()?(异常路径也要释放)

7. 读写锁选择:ReadWriteLock vs StampedLock

根据读写模式选择不同的锁实现:

ReentrantReadWriteLock:
• 读锁共享,写锁独占
• 可重入,支持条件变量
• 写锁可降级为读锁

StampedLock:
• 提供乐观读模式,性能更高
• 不可重入,不支持条件变量
• 通过 stamp 验证锁有效性

// StampedLock 乐观读示例
StampedLock lock = new StampedLock();
int value = 0;

int readValue() {
    long stamp = lock.tryOptimisticRead(); // 乐观读
    int currentValue = value;
    
    if (!lock.validate(stamp)) {          // 验证期间是否有写操作
        stamp = lock.readLock();          // 转为悲观读
        try {
            currentValue = value;
        } finally {
            lock.unlockRead(stamp);
        }
    }
    return currentValue;
}

选型建议

  • 读多写少且性能要求高 → StampedLock;
  • 需要可重入或条件变量 → ReadWriteLock

8. 线程池参数配置实践

线程池 ThreadPoolExecutor 配置需要根据业务特点设计:

核心参数:
• corePoolSize:核心线程数,CPU 密集型建议 Ncpu+1
• maximumPoolSize:最大线程数,I/O 密集型可适当增大
• workQueue:任务队列,推荐有界队列避免 OOM
• keepAliveTime+unit:空闲线程存活时间
• threadFactory:线程工厂,建议设置线程名称和异常处理器
• handler:拒绝策略,根据业务需求选择(Abort(默认)、CallerRuns、Discard、DiscardOldest)

// 线程池配置示例
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                                      // corePoolSize
    16,                                     // maximumPoolSize  
    60, TimeUnit.SECONDS,                   // keepAliveTime
    new LinkedBlockingQueue<>(1000),        // 有界队列
    r -> {                                  // 线程工厂
        Thread t = new Thread(r, "biz-thread-" + counter.getAndIncrement());
        t.setUncaughtExceptionHandler((thread, ex) -> 
            logger.error("Uncaught exception in {}", thread.getName(), ex));
        return t;
    },
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

监控指标:活跃线程数、队列大小、拒绝任务数等需要纳入监控。
任务提交速率远超处理能力怎么办?(背压/限流/降级/拆分池/熔断)

9. CompletableFuture 组合编程

CompletableFuture 提供了强大的异步编程能力:

常用操作:

  • supplyAsync(task, executor): 指定线程池,默认是 ForkJoinPool.commonPool
    • thenApply/thenAccept:转换和消费结果
    • thenCompose:扁平化嵌套 Future
    • thenCombine:合并多个 Future 结果
    • allOf/anyOf:等待所有/任意任务完成

异常处理:
• exceptionally:异常恢复
• handle:统一处理结果和异常
• whenComplete:完成时回调

// 并行调用多个服务并合并结果
CompletableFuture<String> futureA = CompletableFuture.supplyAsync(this::callServiceA, executor);
CompletableFuture<String> futureB = CompletableFuture.supplyAsync(this::callServiceB, executor);

CompletableFuture<String> combined = futureA.thenCombine(futureB, (a, b) -> a + b)
    .exceptionally(ex -> {
        logger.warn("Service call failed, using fallback", ex);
        return "fallback-value";
    });

String result = combined.join();

与 Future 的区别?(可组合、非阻塞回调)

10. ForkJoinPool 工作窃取机制

ForkJoinPool 采用工作窃取(work-stealing)算法提升性能:

核心机制:
• 每个工作线程维护自己的双端队列
• 本地任务 LIFO 执行,窃取其他队列任务时从尾部获取
• 减少竞争,提高 CPU 利用率

并行流注意事项:
• 并行流底层使用 ForkJoinPool.commonPool
• 避免在并行流中执行阻塞 I/O 操作
• 注意 ThreadLocal 上下文传递问题

// 在自定义ForkJoinPool中执行并行流
ForkJoinPool customPool = new ForkJoinPool(4);
try {
    customPool.submit(() -> 
        dataList.parallelStream()
               .map(this::processItem)
               .collect(Collectors.toList())
    ).get();
} finally {
    customPool.shutdown();
}

Java 并发工具实践总结(续)

11. 同步器选择:CountDownLatch vs CyclicBarrier vs Phaser

在实际项目中,根据不同的同步需求选择合适的同步工具非常重要:

CountDownLatch - 一次性门闩
• 典型场景:主线程等待多个子任务完成初始化
• 特点:计数不可重置,等待线程在计数归零后继续执行

// 等待三个服务初始化完成
CountDownLatch latch = new CountDownLatch(3);

executor.execute(() -> {
    initServiceA();
    latch.countDown();
});

executor.execute(() -> {
    initServiceB(); 
    latch.countDown();
});

executor.execute(() -> {
    initServiceC();
    latch.countDown();
});

// 主线程等待所有服务初始化完成
latch.await();
logger.info("所有服务初始化完成");

CyclicBarrier - 可复用栅栏
• 典型场景:多线程数据分片处理,需要同步点
• 特点:可重置,支持栅栏动作(barrier action)

Phaser - 多阶段栅栏
• 典型场景:多阶段任务协作,参与者动态变化
• 优势:支持阶段推进、动态注册/注销参与者

未处理屏障破裂异常(BrokenBarrierException)
异常路径忘记调用 countDown(),导致主线程永久等待
Phaser 相比 CyclicBarrier 的优势?(动态参与者、阶段控制)

12. BlockingQueue 的几种实现差异

不同的 BlockingQueue 实现适用于不同场景:

  • ArrayBlockingQueue:基于数组的有界队列,支持公平策略(减少线程饥饿),固定容量,内存占用可控。

  • LinkedBlockingQueue:基于链表的队列,默认无界(Integer.MAX_VALUE),吞吐量通常更高,但可能导致 OOM。适合任务量相对可控的场景

  • PriorityBlockingQueue:无界优先级队列,消费顺序取决于优先级,非入队顺序,适合需要优先处理某些任务的场景

  • DelayQueue:基于优先级队列的延迟队列,元素需实现 Delayed 接口,适合定时任务、缓存过期等场景

注意:生产禁用无界队列,避免无界队列导致的内存溢出和背压失效问题。

13. ThreadLocal 的正确使用与内存泄漏防范

ThreadLocal 非常适合存储线程上下文信息,但需要谨慎使用。

// 线程安全的日期格式化
private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTER = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

public String formatDate(Date date) {
    try {
        return DATE_FORMATTER.get().format(date);
    } finally {
        // 关键:在线程池环境中必须清理,避免内存泄漏
        DATE_FORMATTER.remove();
    }
}

内存泄漏风险:
• 线程池中的线程会复用,如果不调用 remove(),ThreadLocal 值会一直存在
• 值对象如果较大,会导致严重的内存泄漏
• 建议使用 try-finally 确保清理

InheritableThreadLocal在线程池下为何危险 :在线程池中,子任务可能由不同线程执行,继承语义会错乱,不建议在异步场景使用。

14. 死锁预防与诊断实践

死锁是并发编程中的经典问题,需要从预防和诊断两方面着手:

预防措施:

// 使用 tryLock 避免死锁
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();

public void safeOperation() {
    if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
        try {
            if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    // 临界区操作
                    doCriticalWork();
                } finally {
                    lock2.unlock();
                }
            }
        } finally {
            lock1.unlock();
        }
    }
}

统一加锁顺序是预防死锁的有效方法,确保所有线程以相同顺序获取锁。

诊断工具
• jstack:查看线程状态和锁持有情况
• JFR:Java Flight Recorder 记录详细并发事件
• ThreadMXBean.findDeadlockedThreads():编程式检测死锁

概念区分:
• 死锁:相互等待对方释放锁
• 活锁:线程不断重试但无法前进(如消息处理失败不断重试)
• 饥饿:某些线程长期得不到执行机会

15. ConcurrentHashMap 内部机制理解

核心改进
• 摒弃分段锁,使用 bin-level 锁(synchronized)+ CAS 初始化桶。
• 高冲突时链表转红黑树(treeify),阈值 8/6,提升查询效率
• 扩容时支持多线程协同迁移

使用注意:
• size() 返回的是近似值,因为并发环境下精确计数代价太高
• 复合操作(如 putIfAbsent)仍需外部同步保证原子性
• computeIfAbsent 在并发环境下可能被多次执行,需要保证幂等性

16. 结构化并发改善代码可维护性

结构化并发让异步代码具有同步代码的清晰结构。

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    // 并行发起多个RPC调用
    Supplier<String> userTask = scope.fork(() -> userService.getUser(userId));
    Supplier<String> orderTask = scope.falk(() -> orderService.getOrders(userId));
    
    // 等待所有任务完成或任一失败
    scope.join();
    scope.throwIfFailed();
    
    // 组合结果
    return new UserResponse(userTask.get(), orderTask.get());
}

优势:
• 任务生命周期管理自动化
• 异常传播和取消机制更完善
• 避免"悬挂任务"问题

  • 与 CompletableFuture.allOf 相比,结构化并发提供了更好的可观察性和控制力。

17. 性能优化:减少竞争与伪共享

在高并发场景下,减少竞争是关键优化方向:

减少锁竞争:
• 数据分片(如 LongAdder 的分段计数)
• 读写分离(读写锁、CopyOnWrite)
• 乐观策略(StampedLock 乐观读)

解决伪共享:

// 使用缓存行填充避免伪共享
@jdk.internal.vm.annotation.Contended
public class Counter {
    private volatile long value;
    // 自动添加填充字节,避免与其他热点字段共享缓存行
}

LongAdder 的热点规避原理:通过分桶(Cell数组)分散写压力,sum时合并结果,在高并发写场景下性能远超AtomicLong。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐