【面试突击】JAVA基础知识-volatile、synchronized与ReentrantLock深度对比

22 阅读5分钟

Java 中的 volatile 与锁机制详解


1. volatile 是什么?

在 Java 中,volatile 是一种轻量级同步机制,用来保证变量的可见性禁止指令重排序的一部分行为,但不保证复合操作的原子性

声明方式示例:

volatile int flag;
volatile boolean running;
volatile Object ref;

2. volatile 解决的两个核心问题

2.1 可见性(Visibility)

多线程下,每个线程有自己的工作内存(寄存器、CPU 缓存等),对共享变量的修改可能只在本线程可见。

使用 volatile 后:

  • 一个线程对 volatile 变量的,会立刻刷新到主内存
  • 其他线程读同一个 volatile 变量时,会直接从主内存读取最新值。

示例:线程退出标志

class Worker implements Runnable {
    private volatile boolean running = true;

    @Override
    public void run() {
        while (running) {
            // do work
        }
        System.out.println("Stopped.");
    }

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

如果没有 volatile,某些情况下 while (running) 可能一直读到缓存中的旧值,导致线程无法退出。


2.2 有序性(避免部分重排序)

Java 内存模型(JMM)允许编译器和 CPU 对指令进行重排序,可能导致线程 B 看到线程 A 的某些操作乱序。

  • volatile 变量的:之前对共享变量的写不会被重排序到这次写之后(写屏障)。
  • volatile 变量的:之后的读/写操作不会被重排序到这次读之前(读屏障)。

happens-before 关系

线程 A 对某个 volatile 变量的写 happens-before 线程 B 之后对这个变量的读。


3. volatile 做不到什么?(常见误区)

3.1 不保证复合操作的原子性

i++,本质是“读 -> 加一 -> 写”,volatile 只保证每次都去主内存读/写,不保证操作整体不可分割

错误示例:

volatile int counter = 0;

public void inc() {
    counter++; // 非原子操作
}

多个线程并发调用时,counter 仍会丢失更新,不是线程安全

正确做法:

  • 用原子类
  • 或加锁
import java.util.concurrent.atomic.AtomicInteger;

class Counter {
    private final AtomicInteger counter = new AtomicInteger(0);

    public void inc() {
        counter.incrementAndGet();
    }
}

3.2 不是锁,不提供互斥

volatile 只保证可见性,不保证同一时刻只有一个线程操作。需要互斥时还是要用锁:

  • synchronized
  • ReentrantLock

4. 典型使用场景

4.1 线程停止标志/状态控制

适用于一个线程写标志,多线程读,不涉及复合操作。

class Task implements Runnable {
    private volatile boolean running = true;

    @Override
    public void run() {
        while (running) {
            // do something
        }
    }

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

实际项目更推荐 Thread.interrupt(),但 volatile boolean 仍常见。


4.2 配置信息 / 对象引用的热更新

用于保证对象引用被替换的可见性

class ConfigManager {
    private volatile Config config = loadFromFile();

    public Config getConfig() {
        return config;
    }

    public void reload() {
        this.config = loadFromFile();
    }
}

4.3 Double-Checked Locking 单例的 instance

必须加 volatile,否则可能返回构造未完成的对象。

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

5. Java 内存模型(JMM)中的 volatile

  1. volatile 前的所有写操作“先行发生于”这次 volatile 写。
  2. 若线程 B 读取到线程 A 写入的 volatile 新值,也能看到 A 在此之前对其他共享变量的修改。

6. 对比:volatile / synchronized / AtomicXXX

特性volatilesynchronizedAtomicXXX
可见性
原子性单步读/写有,复合无具体方法有(如自增)
互斥无(CAS自旋)
重排序约束更强
阻塞不会可能(线程挂起)一般不会
典型场景状态标志、热更新指针临界区、多步逻辑计数器、自增、自减

7. 面试速记结论

  • volatile 只保证可见性/有序性,不保证原子性。大家常用它做状态标志、配置热更新、DCL 单例指针等。
  • 多步逻辑或需要原子性的操作就要用 synchronizedAtomicInteger
  • 记住 DCL 单例必须加 volatile,否则会踩坑(构造重排序)。

synchronized vs ReentrantLock 对比


1. 基本用法和特点

一、synchronized

  • 关键字,自动加解锁
  • 锁对象是任意 Object
  • 语法简单,不易死锁
synchronized (lock) {
    // 临界区
}

二、ReentrantLock

  • 类,需手动 lock/unlock
  • 支持可中断、超时、公平锁、多条件队列等高级功能
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

2. 可中断

  • synchronized 等待锁不可中断,即使线程被 interrupt,只是设置标志,不会抛异常或停止等待。
  • ReentrantLock.lockInterruptibly() 可中断,等待锁时收到 interrupt 会抛 InterruptedException,线程可以及时退出。

3. 超时

  • synchronized 不支持超时,只能无限等待。
  • ReentrantLock.tryLock(timeout, unit) 支持超时+可中断,时间内没拿到锁返回 false,期间可被 interrupt。

4. 公平性

  • synchronized 语义上不保证公平(老线程可能被饿死)。
  • ReentrantLock 构造参数可指定公平锁(new ReentrantLock(true)),尽量按请求顺序获取锁。

5. Condition 多条件队列

  • synchronized 每把锁只有一个隐式等待队列 wait/notify
  • ReentrantLock 可通过 newCondition() 创建多个条件队列,每队唤醒/等待单独管理,复杂场景易用。

6. 其他差异

  • 释放锁:synchronized 自动、异常时也会释放;ReentrantLock 必须手动 unlock(常用 finally)。
  • 可重入性:两者都支持。
  • 调试/监控:ReentrantLock 有状态查询方法(如 isLocked),synchronized 没有直接 API。
  • 性能:JDK1.6 后 synchronized 性能很好,ReentrantLock 高并发/复杂要求下更灵活。

7. 总结对比表

维度synchronizedReentrantLock
类型语言关键字 (JVM级)类 (java.util.concurrent.locks)
获取/释放方式自动手动 lock()/unlock()
可重入
可中断获取锁是 (lockInterruptibly)
超时获取锁是 (tryLock(timeout))
公平锁不保证支持构造参数
多条件队列不支持支持 (Condition)
性能简单场景足够高并发/复杂需求更强

8. 面试归纳话术

问:synchronizedReentrantLock 如何选择?

  • 普通同步优先 synchronized,语法简单安全。
  • 需要可中断/超时/公平锁/多条件队列/复杂锁逻辑时,用 ReentrantLock 更佳。
  • 对于最细致的定制场景(如限时/可取消业务)优选 ReentrantLock