01、Java并发编程:学习攻略如何才能学好并发编程?

01、Java并发编程:学习攻略如何才能学好并发编程?一文搞懂分工、同步、互斥

你是不是也有这种感觉:Java并发这东西,书买了一堆,博客看了几百篇,synchronized、Lock、volatile、AQS、ThreadLocal、ConcurrentHashMap、ThreadPoolExecutor 每个单词都认识,拼在一起就头大。面试前背得滚瓜烂熟,一到线上排查问题,jstack 日志一拉,一堆 BLOCKED、WAITING 的线程,大脑当场空白。

我自己当年也是这么过来的。最早一头扎进 JUC 包,ArrayList 换成 CopyOnWriteArrayList,HashMap 换成 ConcurrentHashMap,觉得这就叫并发编程了。直到线上一个订单系统卡死,CPU 使用率 0%,所有业务线程都挂在 Future.get() 上,我才意识到:我学的不是并发,是 JDK API 使用说明书。

这篇文章不讲零散 API,只做一件事:帮你建立一张并发编程的全景地图。这张地图的骨架来自 Doug Lea 对并发问题的拆解,也是我在无数次线上故障排查中反复验证过的思考框架。读完之后,你再看到任何一个并发类、任何一个线上 Bug,都能迅速定位它属于哪一类问题,该用什么工具解决。


文章目录

  • 01、Java并发编程:学习攻略如何才能学好并发编程?一文搞懂分工、同步、互斥
    • 一、痛点场景:线上那些让你凌晨三点跳起来的并发事故
      • [痛点 1:CPU 使用率 0%,服务却全线卡死](#痛点 1:CPU 使用率 0%,服务却全线卡死)
      • [痛点 2:明明加了 synchronized,为什么数据还是丢了](#痛点 2:明明加了 synchronized,为什么数据还是丢了)
      • [痛点 3:单例模式写了十年,上线第一天就 OOM](#痛点 3:单例模式写了十年,上线第一天就 OOM)
    • 二、解决方案:并发编程其实只有三个核心问题
    • 三、是什么:用三种视角把并发编程讲透
      • [3.1 专业解释](#3.1 专业解释)
      • [3.2 大白话](#3.2 大白话)
      • [3.3 生活案例](#3.3 生活案例)
    • 四、为什么要学:不学并发会怎样
      • [4.1 性能层面](#4.1 性能层面)
      • [4.2 正确性层面](#4.2 正确性层面)
      • [4.3 职业发展层面](#4.3 职业发展层面)
    • 五、演进路线:并发编程是怎么一步步发展到今天的
      • [5.1 石器时代:Java 1.0 - 1.4](#5.1 石器时代:Java 1.0 - 1.4)
      • [5.2 JUC 时代:Java 5(JSR 166)](#5.2 JUC 时代:Java 5(JSR 166))
      • [5.3 函数式异步时代:Java 8](#5.3 函数式异步时代:Java 8)
      • [5.4 虚拟线程时代:Java 21](#5.4 虚拟线程时代:Java 21)
    • 六、怎么用:企业项目里的实战代码
      • [6.1 分工:让多个线程同时干活](#6.1 分工:让多个线程同时干活)
        • [6.1.1 正确姿势:手动创建 ThreadPoolExecutor](#6.1.1 正确姿势:手动创建 ThreadPoolExecutor)
        • [6.1.2 并行调用多个下游服务](#6.1.2 并行调用多个下游服务)
        • [6.1.3 生产者-消费者模式](#6.1.3 生产者-消费者模式)
      • [6.2 同步:让线程之间步调一致](#6.2 同步:让线程之间步调一致)
        • [6.2.1 CountDownLatch:等所有子任务完成](#6.2.1 CountDownLatch:等所有子任务完成)
        • [6.2.2 CyclicBarrier:多线程互相等待到同一个屏障](#6.2.2 CyclicBarrier:多线程互相等待到同一个屏障)
      • [6.3 互斥:保护共享变量](#6.3 互斥:保护共享变量)
        • [6.3.1 synchronized:能不加锁就不加锁,加锁就用它](#6.3.1 synchronized:能不加锁就不加锁,加锁就用它)
        • [6.3.2 ReentrantLock:需要高级功能时才用](#6.3.2 ReentrantLock:需要高级功能时才用)
        • [6.3.3 volatile:解决可见性,不解决原子性](#6.3.3 volatile:解决可见性,不解决原子性)
        • [6.3.4 无锁方案:原子类](#6.3.4 无锁方案:原子类)
        • [6.3.5 线程封闭:从根上避免共享](#6.3.5 线程封闭:从根上避免共享)
    • [七、竞品对比:JDK 里那些"长得像"的东西到底怎么选](#七、竞品对比:JDK 里那些"长得像"的东西到底怎么选)
      • [7.1 synchronized vs ReentrantLock](#7.1 synchronized vs ReentrantLock)
      • [7.2 Executors 工厂方法 vs 手动 ThreadPoolExecutor](#7.2 Executors 工厂方法 vs 手动 ThreadPoolExecutor)
      • [7.3 ConcurrentHashMap vs HashMap vs Collections.synchronizedMap](#7.3 ConcurrentHashMap vs HashMap vs Collections.synchronizedMap)
      • [7.4 CountDownLatch vs CyclicBarrier](#7.4 CountDownLatch vs CyclicBarrier)
    • 八、企业项目里的高频使用场景
    • 九、面试官高频面试题(建议背下来)
    • 十、总结

一、痛点场景:线上那些让你凌晨三点跳起来的并发事故

在讲理论之前,先看三个真实发生过的生产事故。这三个事故覆盖了企业里 90% 以上的并发故障类型,看完你就知道为什么必须学并发。

痛点 1:CPU 使用率 0%,服务却全线卡死

这是最诡异的一类故障。监控大盘显示 CPU 闲得发慌,内存也没爆,但接口全部超时,用户疯狂投诉。运维拉一份 jstack 出来一看,所有业务线程都停在同一个地方。

典型的代码长这样:

java 复制代码
// 订单服务:线程池核心线程 20 个
ExecutorService orderPool = new ThreadPoolExecutor(
    20, 20, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>(1000));

// 主任务:处理一笔订单,内部要调用库存服务
public void handleOrder(Order order) {
    Future<StockResult> future = orderPool.submit(() -> queryStock(order));
    StockResult result = future.get(); // 阻塞等子任务
    // ... 后续处理
}

高峰期 20 个核心线程全部被主任务占满,每个主任务都 submit 了一个子任务然后 get() 阻塞等待。子任务排在队列里永远得不到执行,因为没有空闲线程;主任务又永远卡在 get() 上不肯释放线程。整个线程池瞬间死锁,这就是经典的线程池饥饿死锁(Thread Pool Starvation Deadlock)。

更隐蔽的是 CompletableFuture.supplyAsync() 默认用的是 ForkJoinPool.commonPool(),这个池子的线程数默认是 CPU 核数。你在里面做 HTTP 调用、数据库查询这种阻塞操作,几个并发请求就把公共池占满了,整个应用所有用到 CompletableFuture 的地方全部躺平。

痛点 2:明明加了 synchronized,为什么数据还是丢了

新人最容易犯的错:给 i++ 加了 synchronized,觉得万事大吉,结果高并发下计数器还是少加了。或者更隐蔽------写操作加了锁,读操作没加,读到一个半更新的对象,金额字段是新的,状态字段还是旧的,对账系统直接炸了。

java 复制代码
public class BadCounter {
    private int count = 0;

    public void incr() {
        synchronized (this) {
            count++;
        }
    }

    // 读没加锁,可能拿到中间态
    public int get() {
        return count;
    }
}

问题不在锁本身,在于你根本没意识到:共享变量的跨线程可见性、指令重排序、64 位 long 的非原子读写,这些问题就算加了 synchronized 也未必能完全解决------因为你的锁粒度可能选错了,或者你该用 volatile 的地方用了普通变量,编译器和 CPU 偷偷把指令重排了,单线程下跑得好好的,多线程下就是玄学 Bug。

痛点 3:单例模式写了十年,上线第一天就 OOM

java 复制代码
public class ContextHolder {
    private static Map<String, UserContext> CACHE = new HashMap<>();

    public static UserContext get(String userId) {
        return CACHE.computeIfAbsent(userId, id -> loadFromDb(id));
    }
}

这段代码在单线程下毫无问题。高并发下 HashMap 链表在扩容时可能形成环形链表,读操作 CPU 打满,老年代迅速撑爆,Full GC 停不下来。线上一次大促,这个全局共享的 HashMap 被并发扩容打环,读线程陷入死循环,线程池被瞬间耗尽,千万级资损。

这类事故的共同点是:代码在测试环境跑半年没事,一到生产高并发就炸。因为并发 Bug 是概率性的、时机敏感的、不可复现的,你单元测试覆盖率 100% 也抓不到。


二、解决方案:并发编程其实只有三个核心问题

上面三个痛点,看起来五花八门,本质上都能被归纳到三个核心问题上。这是原作者在文章里反复强调的观点,也是 Doug Lea 设计 JUC 包时的底层思考框架。

  • 分工:怎么把一个大任务拆成多个子任务,交给多个线程去做,谁先做谁后做,谁做什么。
  • 同步:任务之间有依赖,一个任务做完了,怎么通知下一个任务开工。
  • 互斥:多个线程同时访问同一个共享变量,怎么保证结果正确。

你以后再看到任何一个并发类、任何一个线上故障,先问自己三个问题:这是分工没做好?同步没做好?还是互斥没做好?分类清楚了,方案自然就出来了。


三、是什么:用三种视角把并发编程讲透

3.1 专业解释

并发编程(Concurrent Programming)是指在操作系统层面,通过时间片轮转让多个线程在单个或多个 CPU 核心上逻辑上同时推进,从而提高资源利用率和系统吞吐量的编程范式。它解决的核心矛盾是:CPU 速度与 I/O 速度之间的数量级差异。一个数据库查询要 50ms,CPU 在这 50ms 里可以执行上亿条指令,不让它闲着就是并发编程存在的全部意义。

在 Java 里,并发的载体是 Thread,底层依赖操作系统的用户态线程调度;Java 5 之后通过 JUC(java.util.concurrent)包提供了一整套分工、同步、互斥的工具;Java 21 引入虚拟线程(Virtual Thread),把线程调度权从操作系统收回到 JVM,进一步降低了并发编程的门槛。

3.2 大白话

你可以把并发编程理解成开餐馆。

一个厨师(单线程)炒菜,客人点 10 个菜,只能一个一个炒,客人等到饿晕。你招 10 个厨师(多线程),同时开工,10 个菜一起出,这就是并发。

但是光招厨师不够:

  • 10 个厨师同时抢一口锅,锅只有一个,这就是互斥问题。
  • 厨师 A 炒完了青菜,需要厨师 B 拿盘子端走,A 怎么知道 B 准备好了?这就是同步问题。
  • 今天来了 100 桌客人,怎么派活?谁炒川菜谁炒粤菜,谁先做谁后做,这就是分工问题。

整个 JUC 包,就是 Doug Lea 给你准备好的一套餐馆管理制度。

3.3 生活案例

再举个例子:你写一篇 10000 字的报告。

  • 单线程:你自己从头到尾写,查资料、写大纲、写正文、排版,串行,一天写 2000 字,五天交稿。
  • 多线程分工:你负责写正文,同学 A 负责查资料,同学 B 负责排版。查资料和写正文可以部分并行,效率翻倍。
  • 同步:你写到一半发现要引用某个数据,停下来等 A 把资料发你,这就是 Future.get();A 查完资料主动发消息告诉你,这就是事件回调。
  • 互斥:你和 B 同时修改同一段 Word 文档,B 刚改完第三段,你又把它覆盖回旧版本,文档就乱了。所以同一时刻只能有一个人改文档,这就是锁。

理解了这三个类比,你就理解了并发编程 80% 的本质。


四、为什么要学:不学并发会怎样

4.1 性能层面

一台 32 核的服务器,如果你只写单线程代码,CPU 利用率上限是 3%。剩下 31 核在那儿空转,你付了 32 核的钱,只用了 1 核。高并发场景下,一个接口串行调三个下游服务,每个 100ms,接口耗时 300ms;用 CompletableFuture 并行调用,耗时 100ms,吞吐量直接翻 3 倍。这就是为什么大厂面试必问并发------因为它直接关系到系统性能和公司服务器成本。

4.2 正确性层面

互联网系统本质上是共享资源的系统。库存只有 100 件,1000 个用户同时下单;账户余额 1000 块,两个客户端同时发起转账。没有正确的互斥保护,超卖、丢单、重复扣款这些资金事故就是必然事件,不是偶然事件。

4.3 职业发展层面

我面过的中高级 Java 岗位,JD 里写"熟悉多线程并发编程"的概率是 100%。初级开发写 CRUD,高级开发要写并发安全的中间件,架构师要设计整个系统的并发模型。并发能力是区分初级和高级的硬门槛。


五、演进路线:并发编程是怎么一步步发展到今天的

5.1 石器时代:Java 1.0 - 1.4

只有最原始的 Thread、Runnable、synchronized、wait/notify。开发者自己 new 线程、自己管生命周期、自己写锁,相当于手工打铁。这个阶段的问题是:线程太贵(一个线程默认栈 1MB),创建销毁开销大,没有线程池,没有并发集合,写出的程序稍微复杂一点就死锁。

5.2 JUC 时代:Java 5(JSR 166)

这是并发编程历史上最重要的一次升级。Doug Lea 把他之前在 util.concurrent 包里的成熟设计贡献给 JDK,才有了今天的 java.util.concurrent。核心成员包括:

  • 分工:ExecutorService、ThreadPoolExecutor、Future、Callable
  • 同步:CountDownLatch、CyclicBarrier、Semaphore
  • 互斥:ReentrantLock、Condition、ReentrantReadWriteLock
  • 并发容器:ConcurrentHashMap、BlockingQueue
  • 原子类:AtomicInteger、AtomicLong

Java 6 又做了两件大事:给 synchronized 做了偏向锁、轻量级锁、自旋锁的优化;引入了 ForkJoinPool。

5.3 函数式异步时代:Java 8

CompletableFuture 登场,把异步编程从"回调地狱"拉回到链式调用风格。同时 StampedLock 提供了比读写锁更快的乐观读模式,LongAdder 在高并发计数场景下比 AtomicLong 性能好一个数量级。

5.4 虚拟线程时代:Java 21

JEP 444 正式预览虚拟线程。以前一个请求一个平台线程,线程数受 OS 限制(几百到几千就顶天);虚拟线程由 JVM 调度,一台机器可以开几百万个,写同步风格代码享受异步性能。配合 JEP 543 结构化并发,这是未来 5 年 Java 并发的主战场。


六、怎么用:企业项目里的实战代码

这一节按分工、同步、互斥三个维度给出可以直接抄进项目的代码。

6.1 分工:让多个线程同时干活

6.1.1 正确姿势:手动创建 ThreadPoolExecutor

阿里巴巴 Java 开发手册明确禁止用 Executors.newXXX() 工厂方法,因为它们的队列要么无界要么允许创建海量线程,容易 OOM。企业里正确的写法是手动 new ThreadPoolExecutor:

java 复制代码
@Configuration
public class ThreadPoolConfig {

    @Bean("orderExecutor")
    public ExecutorService orderExecutor() {
        int cpuCores = Runtime.getRuntime().availableProcessors();
        return new ThreadPoolExecutor(
            cpuCores * 2,                  // 核心线程数:I/O 密集型给 CPU*2
            cpuCores * 4,                  // 最大线程数
            60L, TimeUnit.SECONDS,         // 空闲线程存活时间
            new ArrayBlockingQueue<>(500), // 有界队列,绝不允许无界
            new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build(),
            // 拒绝策略:让调用方自己跑,起到背压作用,而不是丢任务
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
}

参数怎么定?经验公式:

  • CPU 密集型(计算、加密):核心线程数 = CPU 核数 + 1
  • I/O 密集型(查库、调接口):核心线程数 = CPU 核数 * 2 到 * 8
  • 队列一定要有界,拒绝策略一定要显式指定,不要用默认的 AbortPolicy 直接抛异常。
6.1.2 并行调用多个下游服务

一个商品详情页要调用商品、库存、价格、推荐四个服务,串行要 400ms,并行只要 100ms:

java 复制代码
@Service
public class ProductFacade {

    @Autowired
    @Qualifier("orderExecutor")
    private ExecutorService executor;

    public ProductDetailVO detail(Long skuId) throws Exception {
        Future<Sku> skuFuture   = executor.submit(() -> skuService.getById(skuId));
        Future<Stock> stockFuture = executor.submit(() -> stockService.getBySku(skuId));
        Future<Price> priceFuture = executor.submit(() -> priceService.getBySku(skuId));

        // 四个 future 同时在跑,这里 get 才开始阻塞等结果
        Sku sku = skuFuture.get(800, TimeUnit.MILLISECONDS);
        Stock stock = stockFuture.get(800, TimeUnit.MILLISECONDS);
        Price price = priceFuture.get(800, TimeUnit.MILLISECONDS);

        return ProductDetailVO.of(sku, stock, price);
    }
}

注意这里有个坑:你用的是自己定义的 orderExecutor,千万不要用 CompletableFuture.supplyAsync() 默认的 commonPool,否则下游服务慢一点,整个应用的所有异步调用都会被拖垮。

6.1.3 生产者-消费者模式

这是最经典的分工模式,用 BlockingQueue 实现,解耦生产和消费:

java 复制代码
public class OrderQueue {
    private final BlockingQueue<Order> queue = new ArrayBlockingQueue<>(1000);

    // 生产者:下单接口
    public void produce(Order order) throws InterruptedException {
        queue.put(order);
    }

    // 消费者:后台线程池
    public void startConsume(ExecutorService pool) {
        for (int i = 0; i < 4; i++) {
            pool.submit(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        Order order = queue.take();
                        payService.doPay(order);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            });
        }
    }
}

6.2 同步:让线程之间步调一致

6.2.1 CountDownLatch:等所有子任务完成

导出报表,要查 10 个部门的数据,全部查完才能生成 Excel:

java 复制代码
public ReportVO exportReport(List<Long> deptIds) throws Exception {
    CountDownLatch latch = new CountDownLatch(deptIds.size());
    Map<Long, List<Data>> result = new ConcurrentHashMap<>();

    for (Long deptId : deptIds) {
        executor.submit(() -> {
            try {
                result.put(deptId, reportService.queryByDept(deptId));
            } finally {
                latch.countDown(); // 不管成功失败都要减计数
            }
        });
    }

    // 主线程阻塞等待所有任务完成,最多等 30 秒
    boolean done = latch.await(30, TimeUnit.SECONDS);
    if (!done) {
        throw new BizException("导出超时");
    }
    return ReportVO.of(result);
}

关键点:countDown() 一定要放在 finally 里,否则某个子任务抛异常,计数器永远减不到 0,主线程会一直等到超时。

6.2.2 CyclicBarrier:多线程互相等待到同一个屏障

多线程计算矩阵,每个线程算一行,所有行算完才能进入下一步:

java 复制代码
public void parallelMatrix(double[][] matrix) {
    int n = matrix.length;
    CyclicBarrier barrier = new CyclicBarrier(n, () -> {
        // 所有线程到达屏障后,由最后一个到达的线程执行
        System.out.println("所有行计算完成,开始合并");
    });

    for (int i = 0; i < n; i++) {
        final int row = i;
        new Thread(() -> {
            try {
                computeRow(matrix[row]);
                barrier.await(); // 等其他行
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }
}

CountDownLatch 是一次性的,CyclicBarrier 可以复用,这是两者最核心的区别。

6.3 互斥:保护共享变量

6.3.1 synchronized:能不加锁就不加锁,加锁就用它
java 复制代码
public class Account {
    private long balance;

    // 修饰实例方法:锁的是 this
    public synchronized void withdraw(long amount) {
        if (balance < amount) {
            throw new RuntimeException("余额不足");
        }
        balance -= amount;
    }

    // 修饰代码块:锁对象可以自己选,更灵活
    public void transfer(Account target, long amount) {
        // 必须按固定顺序加锁,否则会死锁
        Account first = this.hashCode() < target.hashCode() ? this : target;
        Account second = this.hashCode() < target.hashCode() ? target : this;
        synchronized (first) {
            synchronized (second) {
                this.balance -= amount;
                target.balance += amount;
            }
        }
    }
}
6.3.2 ReentrantLock:需要高级功能时才用
java 复制代码
public class Stock {
    private int count;
    private final ReentrantLock lock = new ReentrantLock(true); // 公平锁

    public boolean deduct() {
        // 尝试拿锁,最多等 1 秒,拿不到就失败,防止线程无限等待
        if (lock.tryLock()) {
            try {
                if (count <= 0) return false;
                count--;
                return true;
            } finally {
                lock.unlock(); // 必须在 finally 里释放
            }
        }
        return false; // 拿不到锁快速失败,让用户稍后重试
    }
}

ReentrantLock 相比 synchronized 的优势:可超时、可中断、可公平、可绑定多个 Condition。代价是必须手动释放锁,忘了写 finally 就是灾难。

6.3.3 volatile:解决可见性,不解决原子性
java 复制代码
public class Switch {
    // 没有 volatile,工作线程可能永远看不到 main 线程的修改
    private volatile boolean running = true;

    public void start() {
        new Thread(() -> {
            while (running) {
                // 处理任务
            }
        }).start();
    }

    public void shutdown() {
        running = false;
    }
}

记住一句话:volatile 保证可见性和有序性,不保证复合操作的原子性。i++ 这种操作哪怕加了 volatile 也还是线程不安全,必须用 AtomicInteger 或加锁。

6.3.4 无锁方案:原子类
java 复制代码
public class Counter {
    // 高并发计数场景,LongAdder 比 AtomicLong 吞吐量高 10 倍以上
    private final LongAdder hit = new LongAdder();

    public void incr() {
        hit.increment();
    }

    public long get() {
        return hit.sumThenReset();
    }
}
6.3.5 线程封闭:从根上避免共享

最好的锁,是不需要锁。如果一个变量只被一个线程访问,根本不存在并发问题。

java 复制代码
// ThreadLocal:每个线程自己一份 SimpleDateFormat,避免多线程抢同一个
public class DateUtils {
    private static final ThreadLocal<SimpleDateFormat> TL =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static String format(Date date) {
        return TL.get().format(date);
    }

    public static void remove() {
        TL.remove(); // 线程池场景必须清理,否则内存泄漏
    }
}

注意:在线程池里用 ThreadLocal 一定要 remove(),因为线程复用,上一个任务的数据会被下一个任务读到。


七、竞品对比:JDK 里那些"长得像"的东西到底怎么选

7.1 synchronized vs ReentrantLock

对比维度 synchronized ReentrantLock
实现层面 JVM 关键字,monitorenter/monitorexit JDK 层基于 AQS 实现
锁释放 自动释放,异常也会释放 必须手动 finally 释放,忘写就死锁
可中断 不可中断,拿不到就死等 可中断,lockInterruptibly()
可超时 不支持 tryLock(timeout) 支持
公平锁 只支持非公平 可选公平/非公平
条件变量 只能用一个 wait set 可绑定多个 Condition,精确唤醒
性能 JDK 6 后大幅优化,低竞争下更快 高竞争下略优,但差距很小
适用场景 一般互斥,90% 场景首选 需要超时、多条件、公平锁时才用

结论:除非你确实需要 ReentrantLock 的高级功能,否则老老实实用 synchronized。它是官方重点优化的方向,代码也更短更不容易写错。

7.2 Executors 工厂方法 vs 手动 ThreadPoolExecutor

对比维度 Executors.newFixedThreadPool 手动 new ThreadPoolExecutor
队列 LinkedBlockingQueue 无界,请求堆积 OOM ArrayBlockingQueue 有界,可控
最大线程数 等于核心线程数,无法扩容 可配置 maxPoolSize
拒绝策略 默认 AbortPolicy 直接抛异常 可自定义 CallerRunsPolicy 背压
线程命名 默认 pool-x-thread-y,日志难排查 自定义 ThreadFactory,有业务名
合规性 阿里手册明令禁止 生产推荐写法

7.3 ConcurrentHashMap vs HashMap vs Collections.synchronizedMap

对比维度 HashMap Hashtable ConcurrentHashMap
线程安全 不安全 安全,全方法 synchronized 安全,分段锁/CAS
锁粒度 无 整个对象 数组桶级别,JDK8 后 CAS+synchronized
并发度 单线程 同时只有一个线程能进 高并发读写不互斥
是否允许 null key/value 允许 不允许 不允许
性能 单线程最快 最差 高并发下最优
适用场景 单线程局部变量 废弃,不要用 全局共享缓存

7.4 CountDownLatch vs CyclicBarrier

对比维度 CountDownLatch CyclicBarrier
计数方向 倒数到 0,一次性 加数到 parties,可重置
等待方 一个或多个线程等 所有参与线程互相等
使用后 不能重置,报废 可以 reset() 复用
典型场景 等 N 个子任务完成 多线程互相等待到齐后一起开工

八、企业项目里的高频使用场景

把上面的知识落到实处,企业里并发编程主要出现在这几个地方:

场景 1:接口聚合。 一个详情页要调 4-5 个下游服务,用 CompletableFuture 或 Future 并行调用,接口耗时从 500ms 降到 120ms。

场景 2:报表导出。 数据量大,一次性查库超时,用线程池分片查询,主线程用 CountDownLatch 等所有分片查完再合并。

场景 3:秒杀库存扣减。 单机用 LongAdder 预扣计数,分布式用 Redis Lua;单机内存态用 ReentrantLock.tryLock() 防止超卖。

场景 4:异步解耦。 用户下单后,发短信、发优惠券、推风控这些非核心流程丢到独立线程池异步执行,主流程只做落库和返回,接口 RT 从 300ms 降到 30ms。

场景 5:配置缓存。 全局配置用 volatile 引用一个不可变对象,后台线程定时刷新引用,读线程无锁读,这就是典型的无锁读写。

场景 6:定时任务分片。 一个定时任务要处理 100 万条数据,用 CyclicBarrier 让 8 个工作线程各处理 12.5 万条,全部完成后再汇总。


九、面试官高频面试题(建议背下来)

Q1:synchronized 锁的是什么?锁升级过程了解吗?

A:普通方法锁 this,静态方法锁 Class 对象,代码块锁括号里的对象。JDK 6 后引入锁升级:无锁 -> 偏向锁 -> 轻量级锁(自旋)-> 重量级锁。锁只能升级不能降级,偏向锁在 JDK 15 后默认关闭。

Q2:volatile 能保证原子性吗?底层原理是什么?

A:不能保证复合操作原子性。底层通过内存屏障(Memory Barrier)禁止指令重排,并强制将工作内存的值刷新到主内存、使其他 CPU 缓存行失效。典型用途是状态标志位和 DCL 单例。

Q3:什么是 AQS?

A:AbstractQueuedSynchronizer,JUC 同步器的底层框架。核心是一个 volatile int state 表示同步状态,一个 CLH 等待队列存放阻塞线程。ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都是基于 AQS 实现的。

Q4:线程池核心参数有哪些?任务提交后执行流程是什么?

A:7 个参数:corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler。流程:核心线程没满就建核心线程;满了进队列;队列满了建非核心线程到 max;max 也满了执行拒绝策略。

Q5:线程池为什么不建议用 Executors?

A:FixedThreadPool 和 SingleThreadPool 用无界队列,请求堆积会 OOM;CachedThreadPool 最大线程数是 Integer.MAX_VALUE,会创建海量线程 OOM。所以生产环境必须手动 new ThreadPoolExecutor。

Q6:死锁发生的四个必要条件?怎么避免?

A:互斥、持有并等待、不可剥夺、循环等待。破坏任意一个即可:固定加锁顺序破坏循环等待;tryLock 超时破坏不可剥夺;一次性申请所有资源破坏持有并等待。

Q7:ConcurrentHashMap 在 JDK 7 和 8 的实现区别?

A:JDK 7 用 Segment 分段锁,默认 16 段,并发度 16;JDK 8 放弃 Segment,改用 数组 + 链表/红黑树,锁粒度细化到每个桶节点,用 CAS + synchronized,并发度大幅提升。

Q8:ThreadLocal 内存泄漏是怎么回事?怎么解决?

A:ThreadLocalMap 的 key 是 ThreadLocal 的弱引用,value 是强引用。ThreadLocal 对象被回收后 key 变 null,但 value 还被 Entry 链引用,线程池里线程不销毁就一直泄漏。解决:用完显式调用 remove()。

Q9:CountDownLatch 和 CyclicBarrier 的区别?

A:CountDownLatch 是一次性的,一个或多个线程等其他线程完成;CyclicBarrier 是可重置的,所有线程互相等待到屏障点后一起继续。

Q10:虚拟线程和传统线程的区别?

A:传统平台线程一对一映射 OS 线程,数量受 OS 限制(几千);虚拟线程是 JVM 调度的轻量级线程,一个机器可以开几百万个,阻塞 I/O 时自动让出底层平台线程,写同步代码享受异步性能。


十、总结

回到文章开头那个问题:怎么才能学好 Java 并发编程?

答案还是那八个字:跳出来看全景,钻进去看本质。

跳出来,就是先建立分工、同步、互斥这张全景图。以后看到任何一个类、任何一个 Bug,先归类:这是分工没分好,还是同步没同步好,还是互斥没保护好。归类清楚了,方案就在眼前。

钻进去,就是对每个核心问题再往深挖一层。比如看到 ReentrantLock,要追问它底层是 AQS;看到 AQS,要追问 CLH 队列和 state 变量;看到 CAS,要追问 CPU 的 lock 前缀指令和缓存一致性协议。技术的本质是背后的理论模型,理论通了,换一门语言、换一个框架,你依然会。

不要一上来就死磕 JUC 源码。先把这张全景图印在脑子里,再在实际项目里反复用、反复踩坑、反复排查线上问题,半年后你再回头看 Doug Lea 的设计,会发现每一个类都恰如其分地放在它该在的位置。

祝你早日从"看啥都眼熟、写啥都不会"进化到"系统一出问题,扫一眼 jstack 就知道哪类问题"。


本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

相关推荐
ScilogyHunter1 小时前
Hercules系列安全MCU功能安全原理详解
java·单片机·安全·hercules
corey931 小时前
Linux 学习 - 第十四周作业
linux·运维·学习
博风1 小时前
eclipse中配置maven,以及在本地使用maven。
java·eclipse·maven
云-缩影1 小时前
实际项目中怎么选排序算法:从面试题到工程落地
java·算法·排序算法
追星星の人1 小时前
java web项目(eclipse软件)全流程备份
java·servlet
OxYGC1 小时前
[工程] PHP2Shell 漏洞解析:两条 PHP 内核 RCE,为什么应用层的防护全部失效
android·开发语言·php
小徐不徐说2 小时前
数据库存储基础:理解表结构、主键、事务与 BLOB
开发语言·数据库·c++·qt·程序设计
草莓熊Lotso2 小时前
【C++ 基于微服务的即时通讯系统】网关基建:cpp-httplib 与 WebSocketpp 从入门到实战
linux·开发语言·数据库·c++·redis·微服务·云原生
李纲明2 小时前
WordPress 媒体库上传失败或一直转圈:先核 PHP 上传限制与目录权限,再查安全插件与对象存储
运维·开发语言·安全·php·wordpress·媒体库·devdiy