Java并发编程:深入理解volatile、线程安全陷阱与复合操作

一、volatile关键字详解

1. 核心作用
  • 可见性:对volatile变量的写操作立即刷新到主内存,读操作直接读取主内存。
  • 有序性:禁止指令重排序(通过内存屏障),确保代码执行顺序符合预期。
  • 局限性:不保证原子性(如i++需配合锁或原子类)。
2. 底层原理
  • JMM层面:插入内存屏障(如StoreLoad屏障),强制缓存同步。
  • 硬件层面:依赖CPU的MESI协议实现缓存行失效。
3. 正确使用场景
  • 状态标志:单次写入的布尔值(如线程退出控制)。
  • 一次性发布:双重检查锁单例模式(禁止对象初始化重排序)。

二、volatile的线程安全与不安全场景

1. 线程安全场景
  • 示例1:状态标志
    volatile boolean running = true;
    void stop() { running = false; } // 安全:单次写入
    
  • 示例2:单例模式(双重检查锁)
    private volatile static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // volatile禁止重排序
                }
            }
        }
        return instance;
    }
    
2. 线程不安全场景
  • 示例1:计数器递增

    volatile int count = 0;
    void increment() { count++; } // 不安全:非原子操作
    
    • 原因count++分解为read-modify-write三步,多线程竞争导致结果错误。
  • 示例2:多变量依赖

    volatile int version = 0;
    volatile String data = "init";
    void update() {
        data = "new";   // 步骤1
        version++;      // 步骤2
    }
    
    • 原因:步骤1和步骤2可能被重排序,其他线程可能看到data更新但version未更新。

三、Java中的单条语句复合操作

1. 常见复合操作
操作 分解步骤 线程安全问题
i++ / i-- 读取 → 修改 → 写入 多线程竞争导致结果不一致
value += 10 读取 → 计算 → 写入 类似i++
long/double赋值 32位JVM分两次写入(高/低32位) 可能读到中间状态值
map.put(key, value) 检查存在性 → 插入数据 检查与插入之间可能被其他线程修改
new Singleton() 分配内存 → 初始化对象 → 赋值引用 指令重排序导致未初始化对象泄露
2. 解决方案
  • 原子类AtomicIntegerAtomicReference
  • 锁机制synchronizedReentrantLock
  • 并发容器ConcurrentHashMapCopyOnWriteArrayList

四、面试题

1. volatile如何实现可见性?与synchronized有何区别?
  • 答案
    volatile通过内存屏障强制刷新主内存和失效其他线程的缓存;synchronized通过锁的获取与释放隐式实现内存同步。
    区别:volatile仅保证可见性和有序性,不保证原子性;synchronized三者均保证。
2. 双重检查锁中volatile的作用是什么?
  • 答案:禁止对象初始化时的指令重排序,确保其他线程不会拿到未完全初始化的对象。
3. 伪共享(False Sharing)是什么?如何解决?
  • 答案:多个volatile变量位于同一缓存行,导致不必要的缓存失效。
    解决方案:填充字节或使用@Contended注解。
4. 以下代码是否线程安全?为什么?
volatile int[] arr = new int[10];
void set(int index, int value) { arr[index] = value; }
  • 答案:不安全。volatile仅保证数组引用的可见性,不保证数组元素的可见性。
5. 如何用volatile实现无锁的线程安全计数器?
  • 答案:无法直接实现。需配合CAS操作(如AtomicInteger)或锁。

五、总结

  • volatile适用场景:单次写入的状态标志、对象安全发布。
  • volatile不适用场景:复合操作(如i++)、多变量原子更新。
  • 复合操作的本质:看似单条语句,实际包含多个步骤,需结合锁或原子类保证线程安全。

你可能感兴趣的:(Java并发编程,java,安全,单例模式)