vipxieliang头像
关注

Java 内存模型入门:volatile、synchronized、happens-before

1. 引言

并发编程是 Java 开发者进阶的必经之路,而理解 Java 内存模型(Java Memory Model,JMM)则是掌握并发安全的基石。很多初学者在编写多线程程序时,会遇到「明明改了变量,另一个线程却看不到」「程序运行结果不稳定」等诡异问题,这些往往都与 JMM 的三大特性——可见性、原子性、有序性——有关。

本文将从 JMM 的核心概念出发,依次讲解 volatile 能解决什么、不能解决什么,synchronized 的锁升级机制,以及 happens-before 规则,最后通过实战案例帮助你把这些知识串起来。

2. 从 JMM 说起:可见性、原子性、有序性

Java 内存模型规定了多线程环境下共享变量的读写规则,其核心是解决三个问题:可见性、原子性、有序性。

2.1 可见性(Visibility)

可见性指的是:当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。

在 JMM 中,每个线程都有自己的工作内存(可以理解为 CPU 缓存),线程对变量的读写都发生在工作内存中,再同步回主内存。如果缺少同步机制,一个线程的修改可能不会及时刷新到主内存,其他线程读到的就是旧值。

public class VisibilityDemo {
    private static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread reader = new Thread(() -> {
            while (!flag) {
                // 空转等待
            }
            System.out.println("flag 变为 true,退出循环");
        });
        reader.start();

        Thread.sleep(100);
        flag = true; // 主线程修改 flag
    }
}

上面的代码中,reader 线程可能永远无法退出循环,因为主线程对 flag 的修改对 reader 线程不可见。

2.2 原子性(Atomicity)

原子性指的是:一个操作要么全部执行成功,要么全部不执行,中间不能被其他线程打断。

Java 中,对基本类型的赋值和读取(如 int x = 1)是原子的,但 i++ 这种「读-改-写」复合操作并不是原子的,它实际上包含三步:读取 i、计算 i+1、写回 i。在多线程环境下,这会导致结果丢失更新。

public class AtomicityDemo {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    count++;
                }
            });
            threads[i].start();
        }
        for (Thread t : threads) {
            t.join();
        }
        System.out.println("count = " + count); // 结果通常小于 100000
    }
}

2.3 有序性(Ordering)

有序性指的是:程序执行的顺序按照代码的先后顺序执行。但为了提升性能,编译器和 CPU 可能会对指令进行重排序(reordering),只要重排序后的结果与单线程执行结果一致即可。在多线程环境下,重排序可能导致意想不到的问题。

public class OrderingDemo {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100000; i++) {
            x = 0; y = 0; a = 0; b = 0;
            Thread t1 = new Thread(() -> { a = 1; x = b; });
            Thread t2 = new Thread(() -> { b = 1; y = a; });
            t1.start(); t2.start();
            t1.join(); t2.join();
            if (x == 0 && y == 0) {
                System.out.println("第 " + i + " 次出现重排序");
            }
        }
    }
}

3. volatile:能解决什么,不能解决什么

volatile 是 Java 提供的最轻量的同步机制,它主要解决可见性和有序性问题。

3.1 volatile 能解决什么

保证可见性:volatile 变量的写操作会立即刷新到主内存,读操作会从主内存读取,从而保证其他线程能看到最新值。

保证有序性:volatile 通过内存屏障禁止指令重排序,保证 volatile 变量读写前后的指令顺序。

public class VolatileDemo {
    private static volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread reader = new Thread(() -> {
            while (!flag) {
                // 空转等待
            }
            System.out.println("flag 变为 true,退出循环");
        });
        reader.start();

        Thread.sleep(100);
        flag = true; // volatile 保证 reader 线程能看到
    }
}

3.2 volatile 不能解决什么

不能保证原子性:volatile 无法解决 i++ 这类复合操作的原子性问题,因为「读-改-写」三步之间仍可能被其他线程打断。

public class VolatileNotAtomicDemo {
    private static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    count++;
                }
            });
            threads[i].start();
        }
        for (Thread t : threads) {
            t.join();
        }
        System.out.println("count = " + count); // 仍然小于 100000
    }
}

适用场景总结:

  • 适合:状态标志位、单次发布不可变对象、作为「轻量级锁」使用。
  • 不适合:需要保证原子性的复合操作、多个变量之间的约束关系。

4. synchronized:锁升级与原理

synchronized 是 Java 内置的同步机制,既能保证原子性,也能保证可见性和有序性。它的底层实现经历了从重量级锁到轻量级锁的演进,这就是所谓的「锁升级」。

4.1 synchronized 的基本用法

public class SynchronizedDemo {
    private int count = 0;

    // 同步方法
    public synchronized void increment() {
        count++;
    }

    // 同步代码块
    public void incrementBlock() {
        synchronized (this) {
            count++;
        }
    }
}

4.2 锁升级过程

Java 6 之后,synchronized 引入了偏向锁、轻量级锁、重量级锁的升级机制,目的是在竞争不激烈时减少锁开销。

有其他线程竞争

自旋失败/竞争加剧

无锁状态

偏向锁

轻量级锁

重量级锁

  • 偏向锁:只有一个线程访问同步块时,锁会偏向该线程,减少不必要的 CAS 操作。
  • 轻量级锁:当有第二个线程竞争时,偏向锁升级为轻量级锁,通过自旋等待获取锁。
  • 重量级锁:当自旋超过阈值或竞争激烈时,升级为重量级锁,依赖操作系统互斥量,线程会阻塞。

4.3 synchronized 与 volatile 的对比

特性volatilesynchronized
原子性不保证保证
可见性保证保证
有序性保证(禁止重排序)保证
性能开销较小较大(有锁升级优化)
适用场景状态标志、单次发布复合操作、临界区

5. happens-before 规则与实战案例

happens-before 是 JMM 定义的一组规则,用来判断两个操作之间是否存在「先行发生」关系。如果 A happens-before B,那么 A 的执行结果对 B 可见,且 A 的指令顺序在 B 之前。

5.1 核心 happens-before 规则

  • 程序顺序规则:同一个线程中,前面的操作 happens-before 后面的操作。
  • 监视器锁规则:对一个锁的解锁 happens-before 后续对这个锁的加锁。
  • volatile 变量规则:对一个 volatile 变量的写 happens-before 后续对这个变量的读。
  • 传递性规则:如果 A happens-before B,B happens-before C,则 A happens-before C。

5.2 实战案例:双重检查锁单例

经典的线程安全单例模式,结合 volatile 和 synchronized:

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;
    }
}

这里 volatile 的作用是防止 new Singleton() 的指令重排序。new 操作实际上包含三步:分配内存、初始化对象、将引用指向内存。如果不加 volatile,编译器可能将后两步重排序,导致其他线程拿到一个未初始化完成的对象。

5.3 实战案例:生产者-消费者模型

利用 volatile 作为状态标志,配合 synchronized 保证临界区安全:

public class ProducerConsumerDemo {
    private static volatile boolean running = true;
    private static final Object lock = new Object();
    private static int buffer = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread producer = new Thread(() -> {
            while (running) {
                synchronized (lock) {
                    buffer++;
                    System.out.println("生产,buffer = " + buffer);
                }
                try { Thread.sleep(50); } catch (InterruptedException e) { }
            }
        });

        Thread consumer = new Thread(() -> {
            while (running) {
                synchronized (lock) {
                    if (buffer > 0) {
                        buffer--;
                        System.out.println("消费,buffer = " + buffer);
                    }
                }
                try { Thread.sleep(80); } catch (InterruptedException e) { }
            }
        });

        producer.start();
        consumer.start();
        Thread.sleep(1000);
        running = false; // volatile 保证两个线程都能看到停止信号
        producer.join();
        consumer.join();
    }
}

6. 总结

本文从 JMM 的三大特性出发,梳理了并发编程的核心知识:

  • 可见性、原子性、有序性是并发安全的三个维度,理解它们是排查并发问题的前提。
  • volatile 解决可见性和有序性,但不解决原子性,适合状态标志等轻量场景。
  • synchronized 通过锁升级机制在保证原子性的同时兼顾性能,是临界区保护的默认选择。
  • happens-before 规则是判断操作间可见性的理论依据,双重检查锁单例是它的经典应用。

在实际开发中,建议优先使用 java.util.concurrent 包下的工具类(如 AtomicInteger、ConcurrentHashMap),它们封装了底层的并发细节,更不容易出错。理解 JMM 的底层原理,能帮助你在遇到疑难并发问题时快速定位根因。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/vipxieliang/article/details/166945457

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--