3
0
0

Java 多线程:线程安全、原子性、可见性与有序性

2026-11-03
2026-11-03
文章摘要
|

一、线程安全与三大特性

1. 什么是线程安全?

线程安全是指:多个线程并发访问共享数据时,程序仍然能够得到符合预期的正确结果。

例如,商品库存为 100,两个线程分别扣减一次库存:

stock--;

正常情况下,最终库存应该是 98。

但在多线程环境中,可能发生:

初始库存:100

线程 A:读取库存 100
线程 B:读取库存 100

线程 A:计算 100 - 1 = 99
线程 B:计算 100 - 1 = 99

线程 A:写回 99
线程 B:写回 99

最终库存:99(错误)

这就是典型的丢失更新(Lost Update)。

原因是 stock-- 并不是一个原子操作,它包含读取、计算和写回三个步骤。

多个线程可能在这些步骤之间交叉执行。

Java 并发编程主要需要解决三个问题:

  • 原子性(Atomicity)

  • 可见性(Visibility)

  • 有序性(Ordering)

2. 原子性

原子性是指一个操作或一组操作作为不可分割的整体执行,不会被其他线程交叉干扰。

例如:

count++;

不是原子操作,因为它实际上包含:

第一步:读取 count
第二步:计算 count + 1
第三步:将结果写回 count

假设两个线程同时执行:

count++;

可能发生丢失更新。

可以使用 AtomicInteger 实现原子递增:

AtomicInteger count = new AtomicInteger(0);

count.incrementAndGet();

AtomicInteger 是 Java 8 自带的 JUC 原子类,主要通过 CAS 实现原子操作。

也可以使用 synchronized:

private static int count = 0;

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

当所有线程都通过该同步方法修改 count 时,可以保证递增操作的互斥性。

3. 可见性

可见性是指一个线程修改共享变量后,其他线程能够按照 Java 内存模型规定的同步关系观察到修改。

例如:

public class VisibilityDemo {

    private static boolean running = true;

    public static void main(String[] args)
            throws InterruptedException {

        Thread worker = new Thread(() -> {

            while (running) {
                // 持续检查状态
            }

            System.out.println("线程退出");
        });

        worker.start();

        Thread.sleep(1000);

        running = false;
    }
}

预期:

主线程修改 running = false 后,子线程退出。

但实际运行时,子线程可能一直循环。

原因是多个线程之间没有建立必要的同步关系。JIT 编译器或处理器可能进行优化,导致子线程没有按预期观察到变量的更新。

解决方法:

private static volatile boolean running = true;

volatile 可以提供跨线程的可见性保证。

注意:

  • volatile 并不保证线程立即获得 CPU 执行。

  • volatile 不会自动终止正在进行的阻塞 IO。

  • volatile 不能保证复合操作的原子性。

4. 有序性

有序性是指程序中的操作必须满足 Java 内存模型规定的执行顺序约束。

Java 为了提高性能,允许编译器和处理器进行指令重排序。

例如:

int a = 10;
int b = 20;
int c = a + b;

在不改变单线程执行结果的前提下,一些没有数据依赖关系的操作可能调整执行顺序。

但在多线程环境下,如果没有正确同步,其他线程可能观察到不符合预期的数据状态。

例如:

public class OrderingDemo {

    private static int data = 0;

    private static boolean ready = false;

    public static void main(String[] args) {

        Thread writer = new Thread(() -> {
            data = 100;
            ready = true;
        });

        Thread reader = new Thread(() -> {
            if (ready) {
                System.out.println(data);
            }
        });

        writer.start();
        reader.start();
    }
}

在没有同步关系的情况下,即使 reader 观察到 ready == true,也不能保证它读取到的 data 一定是 100。

这是因为普通共享变量的访问缺少必要的可见性和有序性保证。

解决方法:

private static volatile boolean ready = false;

写线程:

data = 100;
ready = true;

读线程:

if (ready) {
    System.out.println(data);
}

当读线程读取到写线程通过 volatile 写入的 true 时,可以保证观察到之前完成的 data = 100。

这是 Java 内存模型中 happens-before 规则提供的保证。

二、Java 内存模型 JMM

1. 什么是 JMM?

JMM 全称 Java Memory Model,即 Java 内存模型。

JMM 定义了 Java 程序在多线程环境下共享变量的访问规则,以及原子性、可见性和有序性相关的内存语义。

JMM 可以简单理解为:

           JMM 主内存(抽象概念)
                 |
         共享变量 count
                 |
       ┌─────────┴─────────┐
       |                   |
       v                   v
线程 A 的工作内存       线程 B 的工作内存
       |                   |
       v                   v
    线程 A                线程 B

这里的工作内存是 JMM 的抽象概念,用于描述线程对共享变量的使用方式。

不能直接将它理解为 CPU 缓存或者 Java 虚拟机栈。

2. JMM 与 JVM 内存结构的区别

这两个概念经常被混淆。

对比

JMM

JVM 运行时内存结构

全称

Java Memory Model

JVM Runtime Data Areas

主要作用

规定多线程内存访问语义

定义 JVM 运行时数据区域

关注点

可见性、有序性、同步规则

堆、栈、方法区等

是否等同

否

否

例如:

JVM 内存结构中的堆、虚拟机栈、方法区,描述的是 Java 程序运行期间的数据存储区域。

而 JMM 主要描述多个线程在访问共享数据时,需要遵守哪些规则。

重点:JMM 是内存访问的抽象规范,不是具体的内存区域划分。

3. 什么是 happens-before?

happens-before 是 Java 内存模型中非常重要的概念。

它用于描述两个操作之间的内存可见性与顺序保证。

如果操作 A happens-before 操作 B,那么 A 的执行结果对 B 可见,并且 A 在内存语义上先于 B。

但它并不意味着所有存在 happens-before 关系的操作,都必须在物理执行时间上严格先后执行。

常见规则:

规则

含义

程序次序规则

同一线程中,前面的操作 happens-before 后续操作

监视器锁规则

对锁的 unlock happens-before 后续对同一锁的 lock

volatile 规则

对 volatile 变量的写 happens-before 后续对该变量的读

线程启动规则

调用 Thread.start() happens-before 被启动线程中的操作

线程终止规则

线程中的操作 happens-before 其他线程成功检测到其终止

传递性

A happens-before B,B happens-before C,则 A happens-before C

例如:

private static int data = 0;
private static volatile boolean ready = false;

线程 A:

data = 100;
ready = true;

线程 B:

if (ready) {
    System.out.println(data);
}

当线程 B 读到由线程 A 写入的 true 时:

线程 A:data = 100
         |
         v
线程 A:ready = true
         |
         | volatile 写-读建立 happens-before
         v
线程 B:读取 ready == true
         |
         v
线程 B:读取 data

通过 happens-before 的传递性,可以保证线程 B 观察到 data = 100。

这里要注意:volatile 提供的是同步关系,不是把所有代码都变成原子操作。

三、volatile、synchronized 与 AtomicInteger

1. 三者的核心区别

对比

volatile

synchronized

AtomicInteger

原子性

不保证复合操作原子性

可保证受保护代码互斥执行

保证自身原子操作

可见性

保证

保证

保证

有序性

提供特定内存顺序保证

提供同步顺序保证

提供相应内存语义

实现方式

JMM 内存语义

Monitor 监视器锁

CAS 等原子操作

常见用途

状态标记

多个共享变量的同步操作

并发计数器

2. volatile 为什么不能保证 count++ 的原子性?

例如:

private static volatile int count = 0;

多个线程同时执行:

count++;

仍然可能出现错误。

因为 count++ 包含:

读取 count
    ↓
计算 count + 1
    ↓
写回 count

volatile 可以保证变量访问的可见性和相应的顺序约束,但不能保证这三个步骤整体不可分割。

因此:

volatile int count;

不能让 count++ 变成线程安全的操作。

3. 使用 AtomicInteger 保证原子递增

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {

    private static final AtomicInteger COUNT =
            new AtomicInteger(0);

    public static void main(String[] args) {

        COUNT.incrementAndGet();

        System.out.println(COUNT.get());
    }
}

incrementAndGet() 是原子操作。

其底层主要通过 CAS 实现。

CAS 的核心思想是:

读取当前值
    ↓
检查当前值是否仍然符合预期
    ↓
如果符合,则更新
    ↓
如果不符合,则重新尝试

例如:

线程 A 读取 count = 10
线程 B 读取 count = 10

线程 A CAS(10, 11) 成功

线程 B CAS(10, 11) 失败

线程 B 重新读取 count = 11

线程 B CAS(11, 12) 成功

最终 count = 12

因此不会出现普通 count++ 的丢失更新问题。

4. 使用 synchronized 保证互斥

public class SynchronizedDemo {

    private static int count = 0;

    public static synchronized void increment() {

        count++;
    }
}

因为这是静态同步方法,所以锁对象是:

SynchronizedDemo.class

所有调用该方法的线程都会竞争同一把类对象监视器锁。

在所有相关访问都正确使用同一把锁的前提下,能够保证同步方法中的递增操作线程安全。

四、并发计数器完整测试

下面通过实际代码验证普通变量和 AtomicInteger 的区别。

使用 Java 8 自带的 ExecutorService、CountDownLatch 和 AtomicInteger。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class CounterDemo {

    private static int count = 0;

    private static final AtomicInteger ATOMIC_COUNT =
            new AtomicInteger(0);

    public static void main(String[] args)
            throws InterruptedException {

        int threadCount = 8;
        int loopCount = 100000;

        ExecutorService executor =
                Executors.newFixedThreadPool(threadCount);

        // 控制线程统一开始
        CountDownLatch start = new CountDownLatch(1);

        // 等待所有任务完成
        CountDownLatch done =
                new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {

            executor.execute(() -> {

                try {
                    start.await();

                    for (int j = 0; j < loopCount; j++) {

                        // 普通变量递增
                        count++;

                        // 原子变量递增
                        ATOMIC_COUNT.incrementAndGet();
                    }

                } catch (InterruptedException e) {

                    Thread.currentThread().interrupt();

                } finally {

                    done.countDown();
                }
            });
        }

        try {
            start.countDown();

            done.await();

            System.out.println(
                    "预期结果:" + threadCount * loopCount
            );

            System.out.println(
                    "普通变量:" + count
            );

            System.out.println(
                    "原子变量:" + ATOMIC_COUNT.get()
            );

        } finally {
            executor.shutdown();
        }
    }
}

一次可能的执行结果:

预期结果:800000

普通变量:532846

原子变量:800000

原因:

  • 普通变量 count++ 不是原子操作,可能发生丢失更新。

  • AtomicInteger.incrementAndGet() 保证原子递增。

  • CountDownLatch 用于控制多线程开始执行以及等待任务完成。

注意:普通变量的最终结果不固定,也可能碰巧等于预期值,不代表其具有线程安全保证。

五、生产环境:Spring Boot 库存扣减

1. 错误示例

假设商品库存为 100。

Integer stock = productMapper.selectStock(productId);

if (stock > 0) {

    productMapper.updateStock(productId, stock - 1);
}

在高并发情况下,可能发生:

初始库存:100

请求 A:查询库存 100
请求 B:查询库存 100

请求 A:更新库存 99
请求 B:更新库存 99

最终库存:99

两次扣减只减少了一件商品,发生了丢失更新。

2. 为什么不能只使用 synchronized?

例如:

public synchronized void decreaseStock(Long productId) {

    // 查询库存
    // 更新库存
}

如果系统只有一个 JVM 实例,并且所有相关库存操作都使用同一把锁,可以实现当前 JVM 内的互斥。

但如果 Spring Boot 通过 Docker 部署了三个实例:

Spring Boot 实例 A
        |
Spring Boot 实例 B
        |
Spring Boot 实例 C
        |
        v
      MySQL

每个实例都有自己的 JVM 以及锁对象。

不同实例之间的 synchronized 不会自动互斥。

因此,单机锁不能直接解决分布式并发问题。

3. 推荐方式:数据库条件更新

对于简单库存扣减,可以通过单条 SQL 保证检查和修改作为一个数据库原子更新执行。

UPDATE product
SET stock = stock - 1
WHERE id = #{productId}
  AND stock > 0;

MyBatis 示例:

@Mapper
public interface ProductMapper {

    @Update("UPDATE product " +
            "SET stock = stock - 1 " +
            "WHERE id = #{productId} AND stock > 0")
    int decreaseStock(@Param("productId") Long productId);
}

Service:

@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;

    @Transactional
    public boolean decreaseStock(Long productId) {

        int rows = productMapper.decreaseStock(productId);

        return rows == 1;
    }
}

执行结果:

  • 返回 1:库存扣减成功。

  • 返回 0:库存不足或者商品不存在。

在 MySQL InnoDB 中,单条条件 UPDATE 可以利用数据库的并发控制机制安全地执行库存扣减。

这种方式适用于单库中简单的库存扣减操作。

如果涉及订单创建、支付和重复请求,还需要结合事务、幂等性等机制保证整体业务正确性。

重点:Java 线程安全和数据库并发安全是两个不同层面的问题。

六、重点总结

三大核心特性

特性

解决的问题

常见实现方式

原子性

多线程操作交叉导致数据错误

synchronized、AtomicInteger

可见性

一个线程无法按预期看到其他线程的修改

volatile、synchronized

有序性

缺少同步约束时发生不符合预期的重排序与观察结果

volatile、synchronized

需要记住:

  1. count++ 包含读取、计算、写回,不是原子操作。

  2. volatile 保证可见性和特定有序性,但不能保证复合操作的原子性。

  3. synchronized 可以通过互斥和内存同步规则保护共享数据。

  4. AtomicInteger 主要通过 CAS 实现原子操作。

  5. JMM 是 Java 内存访问的抽象规范,不等同于 JVM 的堆、栈等运行时数据区域。

  6. happens-before 用于描述跨线程操作之间的可见性和顺序保证。

  7. 多个 JVM 实例之间不能直接通过 synchronized 实现互斥。

  8. 数据库条件更新可以解决简单库存扣减的并发安全问题。

七、常见问题与答案

1. 为什么 count++ 在多线程环境下不安全?

因为它包含读取、计算和写回三个步骤,不是原子操作,可能发生丢失更新。

2. volatile 能保证线程安全吗?

需要看具体操作。

对于简单的状态标记,volatile 通常可以满足需求。

但对于 count++ 等复合操作,仅使用 volatile 不能保证线程安全。

3. synchronized 能保证原子性、可见性和有序性吗?

在所有相关线程都通过同一把锁正确同步共享状态的前提下,可以提供所需的互斥、可见性和有序性保证。

4. JMM 和 JVM 内存结构有什么区别?

JMM 规定多线程之间的内存访问语义;JVM 内存结构描述堆、方法区、虚拟机栈等运行时数据区域。

5. 什么是 happens-before?

happens-before 是 JMM 定义的内存顺序关系。

如果 A happens-before B,那么 A 的结果对 B 可见,并且 A 在内存语义上先于 B。

6. AtomicInteger 为什么能够保证原子递增?

因为其递增操作通过 CAS 等原子操作实现,多个线程发生竞争时,失败的更新会重试,避免普通递增操作中的丢失更新。

7. volatile 和 synchronized 有什么区别?

volatile 不提供互斥,适合状态标记等场景;synchronized 提供互斥和同步语义,可以保护包含多个操作的临界区。

8. 为什么多实例部署不能只依赖 synchronized?

因为 synchronized 的锁只在当前 JVM 内有效,不同 JVM 实例拥有各自独立的锁对象,无法直接实现跨进程互斥。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论