找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3924

积分

0

好友

538

主题
发表于 前天 02:10 | 查看: 15| 回复: 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。要保证原子性,需要使用 synchronizedAtomicInteger

四、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 内存泄漏原因及如何解决?

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

Q6:如何保证线程安全?

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

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

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

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

Q8:简述 synchronized 锁升级过程?

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

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

九、总结

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

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

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

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




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

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

GMT+8, 2026-3-10 09:30 , Processed in 0.507910 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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