Java并发编程实战指南

Java 并发编程实战指南

Java 并发是后端工程师的必修课,也是大厂面试必考"三件套"(JVM、并发、MySQL)之一。本文从线程基础讲起,覆盖 synchronized、volatile、JUC 工具、线程池、锁、CAS、ThreadLocal、并发设计模式与常见坑,全部带代码和实战场景。


一、为什么需要并发

1.1 并发解决什么问题

  • 吞吐:一个请求一个线程,10 万请求要能并发处理;
  • 响应:一个任务拆多个子任务并行,快 N 倍;
  • 充分利用 CPU:多核机器不用并发就是浪费。

1.2 并发带来的问题

并发不是免费的------引入三大问题:

  1. 原子性 :count++ 不是原子操作(读-改-写三步);
  2. 可见性:线程 A 改了变量,线程 B 看不到;
  3. 有序性:指令重排导致结果意外。

这三大问题贯穿本文所有内容。

1.3 创建线程的三种方式

java 复制代码
// 方式一:继承 Thread
class MyThread extends Thread {
    @Override
    public void run() { System.out.println("thread run"); }
}
new MyThread().start();

// 方式二:实现 Runnable(推荐)
Runnable task = () -> System.out.println("runnable run");
new Thread(task).start();

// 方式三:Callable + Future(能拿返回值)
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> future = pool.submit(() -> 1 + 2);
int result = future.get();   // 阻塞等待结果

企业实践:几乎不用 new Thread,一律用线程池(见第六章)。


二、线程安全核心机制

2.1 synchronized

作用:保证原子性 + 可见性(释放锁时强制刷新内存)。

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

    // 锁住实例(this)
    public synchronized void increment() { count++; }

    // 静态方法锁 Class 对象
    public static synchronized void staticMethod() { }

    // 代码块(锁对象精确控制)
    public void add() {
        synchronized (this) { count++; }
    }
}

锁的是什么:

  • 实例方法 → 锁 this;
  • 静态方法 → 锁 Class;
  • 代码块 → 锁指定对象。

synchronized 原理(JVM 层面):Monitor(监视器锁),每个对象关联一个 monitor。锁升级:无锁 → 偏向锁 → 轻量级锁(CAS 自旋)→ 重量级锁(OS 互斥量)。JDK6+ 自动优化,不用手动干预。

2.2 volatile

作用 :保证可见性 + 禁止指令重排,不保证原子性。

java 复制代码
// 典型用法:状态标志
public class StopFlag {
    private volatile boolean running = true;

    public void stop() { running = false; }

    public void run() {
        while (running) { /* 干活 */ }
    }
}

为什么必须有 volatile:没有它,线程 B 可能一直用 CPU 缓存的旧值,while 循环停不下来(可见性问题)。

volatile 不能做的:

java 复制代码
private volatile int count = 0;
count++;   // 仍然线程不安全(读-改-写不是原子)

正确姿势 :AtomicInteger(见第四章)。

2.3 synchronized vs volatile

维度 synchronized volatile
原子性 保证 不保证
可见性 保证 保证
有序性 保证 部分(禁止重排)
开销 较大 极小
适用 复合操作 状态标志

三、JMM:Java 内存模型

3.1 什么是 JMM

Java 内存模型规定:线程操作变量要先拷贝到自己的工作内存,操作完刷回主内存。

复制代码
主内存(共享变量)
  ↑ 读/写
线程A 工作内存    线程B 工作内存

问题:线程 A 改了值没刷回,线程 B 读的是旧值------这就是可见性问题。

3.2 happens-before 原则(面试重点)

JMM 保证这些关系下,前面操作对后面可见:

  1. 程序次序规则:单线程内代码顺序;
  2. 锁规则:unlock 对后面的 lock 可见;
  3. volatile 规则:volatile 写对后面的读可见;
  4. 传递性:A 先于 B、B 先于 C → A 先于 C;
  5. 线程启动/结束规则:start() 之前对 run() 可见;线程结束对 join() 之后可见。

面试常问:"为什么单例的双重检查要加 volatile?"------因为对象 new 有指令重排(分配内存→引用赋值→初始化),volatile 禁止重排,防止拿到"半初始化"对象。


四、JUC 原子类与 CAS

4.1 原子类

java 复制代码
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();    // 线程安全自增
count.get();

支持的类型:AtomicInteger / AtomicLong / AtomicBoolean / AtomicReference / LongAdder。

LongAdder(高并发计数首选):

java 复制代码
LongAdder count = new LongAdder();
count.increment();
long total = count.sum();

为什么 LongAdder 更快:AtomicLong 高并发时 CAS 失败重试多;LongAdder 分多个 cell 累加,最后求和------减少竞争。

4.2 CAS 原理

CAS = Compare And Swap:

复制代码
CAS(内存值 V, 期望值 A, 新值 B):
  如果 V == A,则 V = B,成功
  否则失败,重新读再试

优点 :无锁,性能好。

缺点:

  • ABA 问题:值 A→B→A,CAS 判断不出来(AtomicStampedReference 加版本号解决);
  • 自旋开销:竞争激烈时一直空转;
  • 只能保证一个变量。

4.3 实战:自旋锁

java 复制代码
public class SpinLock {
    private final AtomicReference<Thread> owner = new AtomicReference<>();

    public void lock() {
        Thread t = Thread.currentThread();
        while (!owner.compareAndSet(null, t)) {
            // 自旋等待
        }
    }

    public void unlock() {
        Thread t = Thread.currentThread();
        owner.compareAndSet(t, null);
    }
}

五、Lock:可中断的锁

5.1 ReentrantLock vs synchronized

java 复制代码
Lock lock = new ReentrantLock();

lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();   // 必须手动释放!
}

ReentrantLock 额外能力:

  • 可中断 :lock.lockInterruptibly();
  • 超时 :lock.tryLock(3, TimeUnit.SECONDS)------拿不到锁不等死;
  • 公平锁 :new ReentrantLock(true);
  • 多个条件:Condition 精准唤醒。

5.2 Condition

java 复制代码
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();

// 生产者
lock.lock();
try {
    while (queue.isFull()) notFull.await();   // 满了等待
    queue.put(item);
    notEmpty.signal();                        // 唤醒消费者
} finally { lock.unlock(); }

注意 :await() 必须在 while 循环里(防止虚假唤醒)。

5.3 ReadWriteLock 读写锁

读读不互斥、读写互斥、写写互斥------读多写少场景大幅提升性能:

java 复制代码
ReadWriteLock rwLock = new ReentrantReadWriteLock();

public Object get(String key) {
    rwLock.readLock().lock();
    try { return cache.get(key); }
    finally { rwLock.readLock().unlock(); }
}

public void put(String key, Object val) {
    rwLock.writeLock().lock();
    try { cache.put(key, val); }
    finally { rwLock.writeLock().unlock(); }
}

六、线程池(必考+必用)

6.1 为什么用线程池

  • new Thread 开销大(创建销毁要系统调用);
  • 无上限创建线程会 OOM(unable to create new native thread);
  • 线程池:复用线程 + 控制并发 + 任务队列缓冲。

6.2 核心参数

java 复制代码
new ThreadPoolExecutor(
    corePoolSize,      // 核心线程数(常驻)
    maximumPoolSize,   // 最大线程数
    keepAliveTime,     // 非核心线程空闲存活时间
    TimeUnit.SECONDS,
    workQueue,         // 任务队列
    threadFactory,     // 线程工厂(命名,便于排查)
    handler            // 拒绝策略
);

执行流程:

复制代码
提交任务
  → 核心线程没满?→ 新建核心线程
  → 队列没满?→ 进队列
  → 最大线程没满?→ 新建非核心线程
  → 满了 → 拒绝策略

拒绝策略:

  • AbortPolicy:抛异常(默认);
  • CallerRunsPolicy:调用者线程执行(推荐,不丢任务);
  • DiscardPolicy:静默丢弃;
  • DiscardOldestPolicy:丢弃最老任务。

6.3 为什么不推荐 Executors

java 复制代码
Executors.newFixedThreadPool(10);     // 队列无界 → 内存爆
Executors.newCachedThreadPool();      // 线程无上限 → 线程爆

生产标准:手动 new ThreadPoolExecutor,参数明确可控:

java 复制代码
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    4, 8, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),
    new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

6.4 线程池参数怎么定(面试高频)

  • CPU 密集 :核心线程 = CPU 核数 + 1;
  • IO 密集 :核心线程 = CPU 核数 × 2(或按公式 CPU/(1-阻塞系数));
  • 队列大小:按峰值任务量 × 缓冲系数;
  • 拒绝策略:生产用 CallerRunsPolicy(不丢任务)。

6.5 线程池监控

java 复制代码
pool.getPoolSize();          // 当前线程数
pool.getActiveCount();       // 活跃线程
pool.getQueue().size();      // 队列积压
pool.getCompletedTaskCount(); // 已完成任务

生产建议:线程池单独配置类,统一管理、可监控、可动态调参。


七、ThreadLocal

7.1 是什么

每个线程独立一份变量副本------线程隔离。

java 复制代码
ThreadLocal<String> traceId = new ThreadLocal<>();

// 请求进来设置
traceId.set("req-12345");
// 任意地方取
String id = traceId.get();
// 用完必须清理!
traceId.remove();

典型应用:链路追踪 ID、用户上下文、SimpleDateFormat(非线程安全)。

7.2 内存泄漏(大坑)

ThreadLocal 用 ThreadLocalMap 存储,key 是弱引用。线程池的线程不销毁,value 强引用一直留着------不 remove 就泄漏。

java 复制代码
// 错误:set 了不 remove
public void handle() {
    traceId.set(id);
    doBiz();       // 忘 remove
}

// 正确:finally 清理
public void handle() {
    try {
        traceId.set(id);
        doBiz();
    } finally {
        traceId.remove();   // 必须清理
    }
}

Spring 里怎么用:用框架自带的(如 MDC、RequestContextHolder),或自己封装工具类强制清理。


八、并发容器

8.1 常用容器对比

场景 线程安全 说明
HashMap ConcurrentHashMap 分段锁/CAS,读快写快
ArrayList CopyOnWriteArrayList 写时复制,读多写极少
Set ConcurrentSkipListSet / CopyOnWriteArraySet 有序/写少
队列 LinkedBlockingQueue / ArrayBlockingQueue 阻塞队列
有序 Map ConcurrentSkipListMap 跳表实现

ConcurrentHashMap 要点:

  • 不允许 null key/value(HashMap 允许);
  • size() 是近似值(并发下不准);
  • 遍历时不会抛 ConcurrentModificationException(弱一致性)。

8.2 阻塞队列

java 复制代码
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(1000);

// 三组方法(面试必问)
queue.add(x);        // 满则抛异常
queue.offer(x);      // 满则返回 false
queue.put(x);        // 满则阻塞等待(生产用这个)

典型应用:生产者-消费者模式(见第九章)。


九、并发设计模式

9.1 生产者-消费者

java 复制代码
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);

// 生产者(多个线程往队列放)
public void produce(Task task) { queue.put(task); }

// 消费者(线程池消费)
ExecutorService consumers = Executors.newFixedThreadPool(4);
for (int i = 0; i < 4; i++) {
    consumers.submit(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            Task task = queue.take();   // 阻塞取
            process(task);
        }
    });
}

优点:解耦 + 削峰(生产快消费慢时队列缓冲)。

9.2 双重检查单例

java 复制代码
public class Singleton {
    private static volatile Singleton instance;   // volatile 必须有

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次检查(避免加锁开销)
            synchronized (Singleton.class) {
                if (instance == null) {            // 第二次检查(真正创建)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

为什么 volatile :防止 new Singleton() 的指令重排(分配内存→赋值→初始化),别的线程拿到未初始化对象。

9.3 异步结果:Future / CompletableFuture

java 复制代码
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> queryDb());
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> callApi());

// 并行等两个结果
Integer sum = f1.thenCombine(f2, Integer::sum).get();

对比 Future:CompletableFuture 支持回调、组合、编排(thenApply/exceptionally/allOf),是异步编排的首选。


十、常见并发坑

10.1 集合修改异常

java 复制代码
List<String> list = new ArrayList<>();
for (String s : list) {
    if (s.equals("x")) list.remove(s);   // ConcurrentModificationException
}

解决 :Iterator.remove()、removeIf、用并发容器。

10.2 死锁

java 复制代码
// 线程A持有锁1等锁2,线程B持有锁2等锁1 → 死锁

排查:

bash 复制代码
jstack <pid> | grep -A 15 "deadlock"

预防:加锁顺序一致、用 tryLock 超时、缩小锁范围。

10.3 线程池用完不关

java 复制代码
// 应用关闭时
pool.shutdown();                    // 不接收新任务,跑完已提交的
pool.awaitTermination(10, TimeUnit.SECONDS);

不关闭线程池 → 应用退不掉(非 daemon 线程)。

10.4 锁的粒度

java 复制代码
// 错误:锁整个方法(并发全串行)
public synchronized void updateAll() { ... }

// 正确:锁最小范围
public void update() {
    synchronized (this) { onlyCritical(); }
    otherWork();   // 不用锁
}

10.5 静态变量的竞争

java 复制代码
// 多个实例共享静态 SimpleDateFormat(非线程安全)→ 乱

解决:DateTimeFormatter(线程安全)或 ThreadLocal。


十一、并发排查工具箱

复制代码
CPU 高:top -Hp → jstack 看线程栈
死锁:jstack 搜 "deadlock"
线程数爆炸:jstack | grep "java.lang.Thread.State" | sort | uniq -c
内存泄漏(ThreadLocal):MAT 分析
并发 Bug:加日志验证(打印线程名)
压测:JMeter / wrk 模拟并发

本章小结

并发编程的三大支柱:synchronized/volatile 保证线程安全、JUC 工具提供高效组件、线程池管理资源。面试和实战的核心:CAS 原理、线程池参数、锁升级、ThreadLocal 泄漏、CompletableFuture 异步编排。

五篇 CSDN 热门主题文档全部完成:Kafka、Git、JVM、Nginx、Java 并发------覆盖后端开发的核心知识面。

(全文完)

相关推荐
phltxy1 小时前
C 语言自定义类型:结构体、位段、枚举与联合体
c语言·开发语言
Joe_Wang51 小时前
【从0到1学习JVM · 15】对象在堆里来回倒腾,究竟怎样才会进老年代?
java·jvm·学习·垃圾回收
一条小小yu1 小时前
为什么使用springboot
java·spring boot·后端
Elastic 中国社区官方博客1 小时前
Kubernetes attributes processor v1:它对 EDOT Collector 意味着什么
java·大数据·elasticsearch·搜索引擎·贪心算法·kubernetes·全文检索
Sarvartha2 小时前
对象创建与引用
java·开发语言
hasty2 小时前
限制写了却没生效:OpenTelemetry Go 的 Unicode 截断边界
开发语言·后端·golang
大侠归来2 小时前
C语言与C++在基础语法上的区别
java·c语言·c++
小宋10213 小时前
SQL + RAG 混合问答实战:结构化指标与文档证据如何统一路由
java·jvm·人工智能·sql
bro_Java6663 小时前
链表进阶2:双向链表的构建
java·数据结构·链表