📘 本文为「Java 模拟面试总结系列」的第 3 篇,内容由我通过 ChatGPT 模拟问答整理而成,适合用于备战中高级 Java 开发面试。

本篇主题:并发编程与锁机制

# 📌 导航:

[TOC]


# 📚 Java 模拟面试系列目录:

  1. Java 基础与进阶
  2. JVM 面试题与知识点详解
  3. 并发编程与锁机制
  4. MySQL 与 SQL 优化
  5. Spring 核心原理
  6. 系统设计与高并发架构
  7. Redis 高级特性
  8. 注册中心与微服务治理
  9. 项目实战与系统架构设计

# ❓问题 1:你了解 Java 的内存模型(JMM)吗?请说一下可见性、有序性和原子性的问题,以及 Java 是如何通过关键字来解决它们的?

# 1️⃣ 原子性

  • 定义:操作是不可再分的最小单位,中间不会被线程切换打断。
  • Java 中如何保证
    • 原始操作如 i++ 不是原子性的
    • 可以使用:
      • synchronized
      • Lock
      • AtomicInteger (底层用 CAS 实现)

# 2️⃣ 可见性

  • 定义:一个线程对共享变量的修改,另一个线程能及时看到
  • Java 提供的可见性保证
    • volatile :写操作会立刻刷新主内存,读操作直接从主内存读
    • synchronized :解锁时会刷新主内存,获取锁会从主内存读取
    • final :在构造函数中安全发布对象

# 3️⃣ 有序性

  • 定义:程序代码的执行顺序是否和编写顺序一致
  • Java 编译器和 CPU 会进行指令重排序(为了优化性能)
  • Java 如何保证有序性
    • volatile :禁止指令重排序
    • synchronized / Lock :建立 happens-before 关系,保证临界区的代码不乱序执行

# 4️⃣答案:

  • 原子性:操作不可分割, synchronizedLockAtomicXXX 保证。
  • 可见性:一个线程修改,其他线程能立即看到, volatilesynchronized 保证。
  • 有序性:执行顺序按代码顺序或 JMM 保证, volatile 禁止指令重排序, synchronized 建立 happens-before 关系。

# ❓问题 2:volatile 的底层原理及它能保证原子性吗?如果不能,那你遇到 volatile i++ 这样的代码要怎么改?

# 🔍 1. volatile 的作用

  • 保证可见性:对变量的写操作,会立刻刷新到主内存,其他线程读操作一定从主内存读取。
  • 禁止指令重排序:读写操作不会被 JVM 或 CPU 编译器重排。

# 🧠 2. volatile 的底层原理

JDK 层和 JVM 层volatile 的底层实现分为:

✅ Java 层:

  • 被编译为特殊的字节码指令: volatile 修饰字段在字节码中有 volatile 标记。

✅ JVM 层(对应 CPU 内存模型):

  • 会在底层插入 内存屏障(Memory Barrier) 指令:
    • 写操作后插入 StoreStore BarrierStoreLoad Barrier ,保证刷新主内存
    • 读操作前插入 LoadLoad Barrier ,保证读取的是主内存最新值

# ⚠️ 3. 为什么 volatile 不能保证原子性?

举例:

volatile int count = 0;
public void add() {
    count++;
}

count++ 实际执行分为三步:

  1. 读取 count
  2. +1
  3. 写回 count

多线程并发执行时,这三个步骤不是原子的,会导致丢数据。


# ✅ 4. 正确做法?

使用:

AtomicInteger count = new AtomicInteger();
public void add() {
    count.incrementAndGet(); // 原子自增
}

底层用的是 CAS + volatile 变量 实现原子性 + 可见性。

# ✅ 答案:

  • volatile 通过内存屏障保证可见性和禁止重排序。
  • 不能保证复合操作(如 i++ )的原子性。
  • 需用 AtomicInteger 或加锁保证原子性。

# ❓问题 3:你了解 Java 中的线程池吗?请简要说明线程池的 7 个参数含义,以及任务提交后的执行流程。

# ✅ 一、线程池的 7 个核心参数(来自 ThreadPoolExecutor 构造函数)

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,       // 核心线程数
    maximumPoolSize,    // 最大线程数
    keepAliveTime,      // 非核心线程存活时间
    unit,               // 存活时间单位
    workQueue,          // 阻塞队列
    threadFactory,      // 线程工厂(创建线程)
    handler             // 拒绝策略
);
参数名 含义
corePoolSize 核心线程数,始终保留,哪怕空闲
maximumPoolSize 最大线程数
keepAliveTime 非核心线程的最大空闲时间,超过就销毁
unit 上面时间的单位(秒、毫秒等)
workQueue 用来存放等待执行的任务
threadFactory 用于创建新线程,可自定义线程名等
handler 拒绝策略,线程和队列都满后触发

# ✅ 二、任务提交到线程池后会发生什么(执行流程)

1. 当前线程数 < corePoolSize:
    → 直接创建新线程处理任务
2. 当前线程数 ≥ corePoolSize:
    → 尝试放入工作队列(workQueue)
3. 工作队列满了:
    → 如果线程总数 < maximumPoolSize,则创建新线程执行任务
4. 如果线程总数 = maximumPoolSize 且队列也满了:
    → 执行拒绝策略(handler)

# 🔧 常见阻塞队列(workQueue)类型

队列类型 特点
ArrayBlockingQueue 有界队列,FIFO
LinkedBlockingQueue 无界队列(可指定大小),吞吐高
SynchronousQueue 不存储任务,来了就必须被线程处理
PriorityBlockingQueue 按优先级排序处理任务

# 🔧 拒绝策略(handler)

RejectedExecutionHandler handler =
    new ThreadPoolExecutor.AbortPolicy(); // 默认,抛异常
// 其他策略:
CallerRunsPolicy    // 由调用线程执行(慢但不丢)
DiscardPolicy       // 直接丢弃任务
DiscardOldestPolicy // 丢最旧的,尝试提交新任务

# ✅ 实际应用举例(建议你在项目中也封装一份)

ThreadPoolExecutor pool = new ThreadPoolExecutor(
    4, 8, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy()
);

# 🎯 小结记忆口诀:

复制编辑核心最大活多久,排队线程谁来造;
满了拒绝交策略,七大参数记得牢。

# ✅答案:

  • 参数: corePoolSizemaximumPoolSizekeepAliveTimeunitworkQueuethreadFactoryhandler (拒绝策略)。
  • 执行流程:先创建核心线程处理,满了放队列,队列满了再扩展线程,最后拒绝策略。

# ❓问题 4:简述 Java 中的锁有哪些?它们各自的特点和适用场景?

# 1️⃣ 悲观锁(Pessimistic Lock)

  • 假设并发冲突很频繁,所以操作之前先加锁,保证线程互斥。
  • Java 中的典型表现:
    • synchronized 关键字(JVM 层实现)
    • ReentrantLock (JDK 提供的显式锁)

# 2️⃣ 乐观锁(Optimistic Lock)

  • 假设并发冲突很少,操作前不加锁,操作后检测数据是否被修改。
  • 典型实现:
    • 数据库中的 version 字段
    • JDK 的 Atomic 类(CAS 机制)

# 3️⃣ 自旋锁(Spin Lock)

  • 线程不会立即阻塞,而是循环等待锁的释放。
  • 适合锁持有时间非常短的场景,减少线程上下文切换开销。
  • Java 中没有直接暴露自旋锁类,但 ReentrantLockAbstractQueuedSynchronizer 内部有自旋机制。

# ✅ 其他锁分类简述:

锁类型 说明 适用场景
偏向锁、轻量级锁 JVM 层面优化,减少无竞争时加锁开销 绝大多数同步场景
读写锁( ReadWriteLock 读多写少,读共享,写独占 高并发读操作
公平锁和非公平锁 公平锁按顺序获得锁,非公平锁效率更高 需要避免线程饥饿时选公平锁

# ✅答案:

  • 悲观锁: synchronizedReentrantLock
  • 乐观锁:版本号机制、 AtomicXXX (CAS)。
  • 自旋锁:短时间等待锁,自旋减少上下文切换。
  • 还有读写锁、公平锁等。

# ❓问题 5:请简述 Java 中 volatilesynchronized 的区别?

#volatilesynchronized 的区别

维度 volatile synchronized
作用 保证变量的可见性和禁止指令重排序 保证可见性、原子性和有序性
使用范围 只能修饰变量 可修饰代码块、方法,锁对象不同(对象锁 / 类锁)
锁机制 不加锁,不阻塞线程 通过获取对象监视器实现阻塞和同步
性能开销 轻量级,性能开销小 相对较重,涉及线程阻塞和唤醒
原子性 不保证原子性 保证操作的原子性
适用场景 变量状态标志、状态变化通知 需要保证复合操作线程安全,临界区代码同步

# ✅ 简单示例

  • volatile

    private volatile boolean flag = true;

    用于多个线程之间快速通知状态变化。

  • synchronized

    public synchronized void increment() {
        count++;
    }

    用于保证代码块执行的互斥,防止并发数据错误。


# ✅ 额外补充

  • volatile 不会阻塞线程,也不保证多个操作的整体原子性(比如 count++ )。
  • synchronized 会阻塞,且能保证执行代码块期间线程独占锁。

#总结

volatile 用于保证共享变量的可见性和防止指令重排序,适合轻量级的状态标识,不保证原子性;而 synchronized 是重量级锁,保证代码块的互斥执行,确保了操作的原子性和可见性,但性能开销较大。

特性 volatile synchronized
保证 可见性、禁止重排序 原子性、可见性、有序性
用法 只能修饰变量 修饰方法或代码块
性能 轻量级 重量级(阻塞)
适用 状态标识 复杂同步操作

📖 敬请期待下一篇:《MySQL 与 SQL 优化》