找回密码
立即注册
搜索
发回帖 发新帖

6519

积分

0

好友

821

主题
发表于 2026-3-8 02:10:53 | 查看: 324| 回复: 0

“并发编程不难,难的是并发中的细节。”

对于很多Java开发者来说,简历上写着“精通并发编程”并不少见。但一到面试追问细节,很多人就露怯了:知道synchronized但说不清底层原理、会用线程池但不知如何调参、了解volatile却讲不清它和synchronized的核心区别。

会用的最多叫熟悉API,懂原理的才算是合格的工程师。

本文将系统性地梳理Java并发编程的核心知识点,尤其是那些在面试求职中高频出现的考点。从基础概念到实用工具,帮你把知识串联起来,不再惧怕追问。

一、线程基础:先搞清楚这些概念

1.1 进程和线程的区别

进程与线程区别对比图

对比项 进程 线程
定义 正在运行的程序 进程内的执行单元
资源 独立内存空间 共享进程资源
开销 大(创建/切换慢) 小(创建/切换快)
通信 复杂(管道/消息队列) 简单(共享内存)
独立性 独立 依赖进程

通俗理解:进程是工厂,线程是工厂里的工人。工人共享工厂的资源(设备、原料),但各自干各自的活。

1.2 线程的创建方式

Java 中创建线程主要有四种方式:

// 方式一:继承 Thread 类
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running");
    }
}
new MyThread().start();

// 方式二:实现 Runnable 接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable running");
    }
}
new Thread(new MyRunnable()).start();

// 方式三:实现 Callable 接口(带返回值)
class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Result";
    }
}
FutureTask<String> task = new FutureTask<>(new MyCallable());
new Thread(task).start();
String result = task.get();

// 方式四:线程池(推荐)
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> System.out.println("Pool running"));
executor.shutdown();

四种方式对比:

方式 优点 缺点 适用场景
Thread 简单 Java单继承受限 简单任务
Runnable 灵活 无返回值 通用场景
Callable 有返回值 稍复杂 异步计算
线程池 线程复用、便于管控 需合理配置参数 生产环境

1.3 线程的状态

Java线程生命周期状态图

六种状态详解:

状态 说明 触发方式
NEW 创建,未启动 new Thread()
RUNNABLE 可运行 start()
BLOCKED 阻塞,等待锁 synchronized 争用
WAITING 无限期等待 wait() / join() / LockSupport.park()
TIMED_WAITING 超时等待 sleep() / wait(timeout) / join(timeout)
TERMINATED 已终止 执行完成

1.4 线程操作方法

// 线程等待
thread.join();       // 等待线程执行完成
Thread.sleep(1000);  // 休眠(不释放锁)

// 线程停止(不推荐用 stop())
thread.interrupt();   // 中断线程
thread.isInterrupted(); // 检查中断状态
Thread.interrupted();  // 静态方法,检查并清除中断状态

// 线程让步
Thread.yield();      // 让出 CPU 时间片(提示调度器)

// 线程守护
thread.setDaemon(true);  // 设置为守护线程

二、synchronized:最熟悉的陌生人

2.1 synchronized 底层原理

synchronized 是 Java 中最常用的同步关键字,很多人知道用,却对底层原理一知半解。

从字节码层面看:

// 编译后会产生 monitorenter 和 monitorexit 指令
public synchronized void method() {
    // ...
}
// 字节码:
// monitorenter
// ... 方法内容 ...
// monitorexit

对象头结构(Mark Word):

锁状态 25位 31位 1位 4位 1位 2位
无锁 对象哈希码 分代年龄 偏向锁位 01
偏向锁 线程ID Epoch 分代年龄 偏向锁位 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向 monitor 的指针 10
GC标记 空 11

2.2 synchronized 锁升级过程

synchronized锁升级过程示意图

锁升级过程:

  1. 偏向锁:第一个线程获取锁,Mark Word 记录线程 ID。
  2. 轻量级锁:多个线程发生竞争,采用自旋方式等待。
  3. 重量级锁:自旋等待超过一定阈值,升级为操作系统级的重量级锁。

面试必问:为什么 synchronized 要设计成可升级的?
答:核心是为了性能优化。偏向锁在无竞争时开销最小;轻量级锁在少量竞争时,自旋等待比直接线程阻塞更高效;只有在大量、激烈竞争时,才使用开销较大但保证公平性的重量级锁。

2.3 synchronized 的四种用法

// 1. 修饰代码块(指定锁对象)
synchronized(this) {
    // 临界区
}

// 2. 修饰实例方法(锁当前对象实例)
public synchronized void method() {
    // 临界区
}

// 3. 修饰静态方法(锁类的 Class 对象)
public synchronized static void method() {
    // 临界区
}

// 4. 修饰类(指定锁类)
synchronized(MyClass.class) {
    // 临界区
}

2.4 synchronized 和 ReentrantLock 的区别

对比项 synchronized ReentrantLock
锁类型 隐式锁,JVM 内置 显式锁,需要手动管理
获取/释放 自动(进入/退出代码块) 手动 (lock() / unlock())
公平锁 不支持 支持(构造时可指定)
响应中断 不支持 支持 (lockInterruptibly())
尝试非阻塞获取 不支持 支持 (tryLock())
条件变量 无 支持多个 Condition
底层实现 Monitor(管程) AQS(AbstractQueuedSynchronizer)
// ReentrantLock 示例
ReentrantLock lock = new ReentrantLock();

// tryLock() 尝试获取(非阻塞)
if (lock.tryLock()) {
    try {
        // 临界区
    } finally {
        lock.unlock(); // 务必在 finally 中释放
    }
}

// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);

// 条件变量
Condition condition = lock.newCondition();
condition.await();  // 等待
condition.signal(); // 通知

三、volatile:最轻量的同步

3.1 volatile 的作用

volatile 是 Java 中最轻量级的同步机制,它保证两个核心特性:

// volatile 保证:
volatile boolean flag = false;

// 1. 可见性:一个线程修改后,新值对其他线程立即可见
// 2. 有序性:禁止指令重排序

可见性演示:

public class VolatileDemo {
    // 不带 volatile,程序可能因可见性问题永远不停止
    // 带 volatile,保证修改对所有线程可见,程序能正确停止
    volatile boolean running = true;

    public void stop() {
        running = false;
    }

    public void run() {
        while (running) {
            // 业务逻辑
        }
    }
}

3.2 有序性:指令重排

// 指令重排示例
int a = 1;      // 1
int b = 2;      // 2
int c = a + b;  // 3

// 编译器或处理器可能为了优化,将顺序重排为:
int b = 2;      // 2
int a = 1;      // 1
int c = a + b;  // 3

// volatile 变量可以禁止其前后的指令进行重排序
volatile int x = 1;
int y = 2;
// x 的读/写操作不会被重排序到 y 的操作之前或之后

3.3 volatile 和 synchronized 的区别

对比项 volatile synchronized
作用域 修饰变量 修饰代码块或方法
原子性 不保证(如 i++) 保证
可见性 保证 保证
有序性 保证(禁止重排) 保证
性能开销 很低(读接近普通变量) 较高(涉及锁升级)

面试必问:volatile 为什么不保证原子性?
答:volatile 只保证单次读/写操作的原子性和可见性、有序性。但像 i++ 这样的复合操作(读取、加1、写入),在多线程下,可能发生线程 A 读取后,线程 B 也读取了旧值,然后各自加1写回,导致最终结果只增加了1。要保证原子性,需要使用 synchronized 或 AtomicInteger。

四、ThreadLocal:线程本地变量

4.1 ThreadLocal 是什么

ThreadLocal 为每个线程提供独立的变量副本,实现了线程间的数据隔离。

// ThreadLocal 示例
public class ThreadLocalDemo {
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        // 线程1
        new Thread(() -> {
            threadLocal.set(1);
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        }, "T1").start();

        // 线程2
        new Thread(() -> {
            threadLocal.set(2);
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        }, "T2").start();
    }
}
// 输出:
// T1: 1
// T2: 2

4.2 ThreadLocal 原理

ThreadLocal原理与内存结构图

4.3 内存泄漏问题

// ThreadLocal 内存泄漏原因:
// ThreadLocalMap 的 Entry 继承自 WeakReference<ThreadLocal<?>>
// key(ThreadLocal 实例)是弱引用,value(存储的值)是强引用

// 解决方案:使用完毕后手动 remove
try {
    threadLocal.set(value);
    // 执行业务逻辑
} finally {
    threadLocal.remove();  // 非常重要!
}

面试加分项:为什么 Entry 的 key 要设计成弱引用?
答:如果 key 使用强引用,那么即使外部不再持有 ThreadLocal 对象的引用(threadLocal = null),由于 ThreadLocalMap 的 Entry 仍持有强引用,ThreadLocal 对象也无法被回收,导致内存泄漏。设计成弱引用后,在 GC 时,ThreadLocal 对象可以被回收,key 会变为 null。但 value 仍然是强引用,如果线程长期运行(如线程池场景),value 无法被访问也无法回收,依然会造成内存泄漏。因此,最佳实践是必须手动调用 remove()。

五、线程池:并发编程的核心

5.1 为什么要用线程池

对比项 传统方式(每次 new Thread) 线程池
创建开销 大(约 1-2 MB) 小(线程复用)
响应速度 慢(需创建线程) 快(有现成线程)
资源管理 无,容易 OOM 可控制最大并发数
线程数量 不可控 可控,避免资源耗尽

5.2 线程池的七大参数

public ThreadPoolExecutor(
    int corePoolSize,              // 核心线程数
    int maximumPoolSize,           // 最大线程数
    long keepAliveTime,            // 空闲线程存活时间
    TimeUnit unit,                 // 时间单位
    BlockingQueue<Runnable> workQueue,  // 阻塞队列
    ThreadFactory threadFactory,   // 线程工厂
    RejectedExecutionHandler handler // 拒绝策略
) {
    // ...
}

5.3 线程池的执行流程

线程池任务执行流程图

5.4 四种常见线程池

线程池 特点 适用场景
FixedThreadPool 固定线程数,无界队列 任务量稳定、可控
CachedThreadPool 线程数弹性,使用 SynchronousQueue 短时、异步、小任务
SingleThreadExecutor 单线程,任务队列 任务需串行执行
ScheduledThreadPool 定时/周期性执行 延迟任务、定期任务
// 阿里巴巴开发规范:禁止使用 Executors 快捷创建线程池
// 应使用 ThreadPoolExecutor 明确参数,避免资源耗尽风险

// 正确写法
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,                      // corePoolSize
    10,                     // maximumPoolSize
    60L,                    // keepAliveTime
    TimeUnit.SECONDS,       // unit
    new LinkedBlockingQueue<>(100),  // workQueue
    new ThreadPoolExecutor.CallerRunsPolicy()  // handler
);

5.5 线程数设置

// CPU 密集型(计算任务多):线程数 ≈ CPU 核心数 + 1
int cpuCores = Runtime.getRuntime().availableProcessors();
int poolSize = cpuCores + 1;

// IO 密集型(数据库、网络操作多):线程数 ≈ CPU 核心数 * 2
// 更精确公式:线程数 = CPU核心数 * (1 + (IO耗时 / CPU耗时))

六、并发容器:安全的数据结构

6.1 ConcurrentHashMap

// JDK 8 之前:分段锁(Segment)
// JDK 8 之后:数组 + 链表/红黑树 + CAS + synchronized(锁粒度更细)

// 常用操作
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);           // 原子 put
map.get("key");              // 高效 get(无锁)
map.computeIfAbsent("key", k -> 1);  // 原子计算,不存在则添加
map.putIfAbsent("key", 1);   // 原子插入(不存在才放)

6.2 常用并发容器

容器 特点 适用场景
ConcurrentHashMap 高并发哈希表 缓存、计数器
ConcurrentLinkedQueue 非阻塞无界队列 高性能生产者-消费者
CopyOnWriteArrayList 写时复制,读快写慢 读多写少(监听器列表、配置)
BlockingQueue 阻塞队列 经典生产者-消费者模型
ConcurrentSkipListMap 并发有序映射 需要排序的缓存
// BlockingQueue 示例(生产者-消费者)
BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);

// 生产者
for (int i = 0; i < 20; i++) {
    final int taskId = i;
    new Thread(() -> {
        try {
            queue.put("task-" + taskId); // 队列满则阻塞
            System.out.println("生产: task-" + taskId);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }).start();
}

// 消费者
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        try {
            String task = queue.take(); // 队列空则阻塞
            System.out.println("消费: " + task);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }).start();
}

七、JUC(java.util.concurrent)工具类

7.1 CountDownLatch

// 倒数计数器:等待多个线程完成
CountDownLatch latch = new CountDownLatch(3);

// 3 个工作线程
for (int i = 0; i < 3; i++) {
    final int id = i;
    new Thread(() -> {
        try {
            Thread.sleep(1000 * (id + 1));
            System.out.println("线程" + id + "完成");
            latch.countDown();  // 计数减1
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }).start();
}

// 主线程等待所有工作线程完成
latch.await();  // 阻塞,直到计数变为 0
System.out.println("所有线程完成,主线程继续");

7.2 CyclicBarrier

// 循环栅栏:让多个线程在某个点相互等待,再一起继续
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程到达栅栏,执行回调"); // 全部到达后执行
});

// 3 个线程
for (int i = 0; i < 3; i++) {
    final int id = i;
    new Thread(() -> {
        try {
            System.out.println("线程" + id + "到达栅栏1");
            barrier.await();  // 等待其他线程
            System.out.println("线程" + id + "继续执行");
            barrier.await();  // 可以复用,进行下一轮等待
        } catch (InterruptedException | BrokenBarrierException e) {
            Thread.currentThread().interrupt();
        }
    }).start();
}

7.3 Semaphore

// 信号量:控制并发访问的线程数量(限流)
Semaphore semaphore = new Semaphore(3);  // 最多允许 3 个线程并发

// 10 个任务
for (int i = 0; i < 10; i++) {
    final int id = i;
    new Thread(() -> {
        try {
            semaphore.acquire();  // 获取许可,没有则阻塞
            System.out.println("线程" + id + "获得许可,开始执行");
            Thread.sleep(1000);
            System.out.println("线程" + id + "执行完成,释放许可");
            semaphore.release();  // 释放许可
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }).start();
}

八、面试高频问题汇总

Q1:synchronized 和 ReentrantLock 的区别?

(答案详见上文 2.4 表格)

Q2:volatile 和 synchronized 的区别?

(答案详见上文 3.3 表格)

Q3:线程池的执行流程?

  1. 提交任务。
  2. 判断核心线程池是否已满?未满 → 创建核心线程执行任务。
  3. 核心线程池已满 → 任务进入阻塞队列等待。
  4. 队列已满 → 判断最大线程池(非核心线程)是否已满?未满 → 创建临时线程执行任务。
  5. 最大线程池也已满 → 执行拒绝策略。

Q4:CountDownLatch 和 CyclicBarrier 的区别?

对比项 CountDownLatch CyclicBarrier
主要用途 等待一个或多个事件完成 等待多个线程到达集合点
计数 单向减少(countDown()),不可重置 可循环使用(await()后重置)
线程关系 发射后不管,不关心谁完成 线程之间相互等待
结束方式 由外部线程调用 countDown() 由参与线程自身调用 await()

Q5:ThreadLocal 内存泄漏原因及如何解决?

原因:ThreadLocalMap 的 Entry 中,key(ThreadLocal 对象)是弱引用,value(存储的值)是强引用。当 ThreadLocal 外部引用被置 null 后,GC 会回收 key,但 value 由于仍被 Entry 强引用而无法回收,且 key=null 的 Entry 无法被访问,导致内存泄漏。
解决:务必在使用完毕后调用 threadLocal.remove()。

Q6:如何保证线程安全?

这是一道综合性问题,可以分层回答:

  1. 互斥同步:synchronized(JVM 级别)、ReentrantLock(JDK 级别)。
  2. 非阻塞同步:Atomic 原子类(CAS 操作)。
  3. 线程本地存储:ThreadLocal,避免共享。
  4. 使用线程安全容器:ConcurrentHashMap、CopyOnWriteArrayList 等。
  5. 不可变对象:使用 final 修饰字段,创建后状态不可变。

Q7:线程池参数(核心线程数)怎么设置?

  • CPU 密集型:核心线程数 ≈ CPU 核心数 + 1。
  • IO 密集型:核心线程数 ≈ CPU 核心数 * 2。更精确的公式是:核心线程数 = CPU核心数 * (1 + (IO耗时 / CPU耗时))。
  • 在实际高并发系统中,这只是一个起点,必须配合压力测试来最终确定。

Q8:简述 synchronized 锁升级过程?

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

  • 偏向锁:假设只有一个线程访问,Mark Word 记录线程 ID,降低无竞争时的开销。
  • 轻量级锁:发生轻度竞争,线程通过 CAS 自旋尝试获取锁,避免直接阻塞。
  • 重量级锁:竞争激烈或自旋超时,升级为操作系统互斥量(mutex),线程进入阻塞队列。

九、总结

并发编程的核心可以归结为三点:同步、线程安全、性能。

  • 同步:解决竞态条件,核心是 synchronized、Lock 和 volatile。
  • 线程安全:保证数据一致性,手段包括原子类、并发容器和 ThreadLocal。
  • 性能:合理利用资源,核心是线程池和各类 JUC 工具类。

希望通过本文的梳理,能帮助你构建起Java并发编程的知识体系。从理解原理到应对面试,再到实际应用,每个环节都至关重要。扎实的基础是解决复杂高并发问题的前提。

如果你想与更多同行交流这些技术细节,或者获取更多实战案例,欢迎来到 云栈社区 ,这里聚集了众多乐于分享和讨论的开发者。




上一篇:解读AI工作流:Prompt、Agent、Skill等核心概念的区别与应用场景
下一篇:OpenClaw开源AI Agent全面解析:架构、安全与2026年生态评估
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-11-8 04:03 , Processed in 0.104848 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表