前言

当多个线程同时操作同一个共享数据(尤其是读写交叉或修改)时,由于CPU的并发执行机制,可能导致数据不一致或丢失,即线程安全问题。

一、多线程操作共享数据的线程安全问题

1. 核心原因:JVM 内存模型

JVM 内存模型将内存分为堆内存(所有线程共享)和栈内存(每个线程私有):

  • 线程操作数据时,会先把主内存的共享数据拷贝到自己的工作内存
  • 线程修改数据后,再把工作内存的数据写回主内存
  • 多线程并发时,由于CPU的执行和线程切换速度很快,这个 “拷贝 - 修改 - 写回” 的过程如果不同步,就会导致数据不一致(比如线程 A 修改了工作内存的数据,但还没刷回主内存,线程 B 就读取了主内存的旧数据)

2. 原子操作的关键

原子操作是 CPU 级别不可中断的指令:

  • 简单赋值(如 int a = 1)是原子操作

  • 复合操作(如 count++)不是原子操作

    在CPU指令角度看,本质上是三个操作:

    • 把内存中的数据加载到CPU的寄存器中

    • 把寄存器中的寄存器进行加一运行算

    • 把寄存器中的数据写回到内存中

  • 线程安全的解决

    把非原子操作 变成 “原子性” 的操作,通过锁实现

二、synchronized 同步锁

1.synchronized内置锁

  • 作用:确保同一时刻只有一个线程可以执行被synchronized修饰的代码块或方法,从而避免并发冲突
  • 实现:当一个线程进入synchronized代码块时,会自动加锁;执行完代码块后,自动解锁
  • 注意事项:synchronized的参数必须是一个对象实例,且该对象需为所有相关线程所共享

2. 代码示例

// 自定义线程类1:循环5000次累加
class ThreadDemo2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5000; i++) {
            // 锁对象是ThreadDemo类的static对象,所有线程共享
            synchronized (ThreadDemo.lockObj) {
                ThreadDemo.count++;
            }
        }
    }
}

// 自定义线程类2:循环10000次累加
public class ThreadDemo extends Thread {
    // 共享数据:所有线程操作的同一个count
    static int count = 0;
    // 全局唯一的锁对象:所有线程共享
    static Object lockObj = new Object();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            // 加锁:同一时刻只有一个线程能执行count++
            synchronized (lockObj) {
                count++;
            }
        }
    }

    public static void main(String[] args) {
        // 创建4个线程:3个ThreadDemo(各累加10000) + 1个ThreadDemo2(累加5000)
        ThreadDemo td1 = new ThreadDemo();
        ThreadDemo td2 = new ThreadDemo();
        ThreadDemo td3 = new ThreadDemo();
        ThreadDemo2 td4 = new ThreadDemo2();

        // 启动所有线程
        td1.start();
        td2.start();
        td3.start();
        td4.start();

        // join():等待所有线程执行完毕,再输出结果
        try {
            td1.join();
            td2.join();
            td3.join();
            td4.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        // 预期结果:3*10000 + 5000 = 35000(加锁后结果准确,不加锁会小于35000)
        System.out.println("已卖出:" + count);
    }
}

三、volatile 关键字

1. 核心特性

  • 作用:主要用于解决变量的“可见性”问题。当一个线程修改了volatile变量,其他线程会立即看到这个修改
  • 禁止指令重排序:JVM 不会对 volatile 变量相关的指令进行重排序(保证执行顺序)

2. 代码示例

// 线程A
class ThreadDemo3 extends Thread {
    // volatile修饰:保证可见性,线程B修改后线程A能立即看到
    volatile boolean isFlag = false;

    @Override
    public void run() {
        System.out.println("线程A启动");
        while (true) {
            System.out.print("");
            if (isFlag) {
                break; 
            }
        }
        System.out.println("线程A执行完成了");
    }
}

// 线程
class ThreadDemo4 extends Thread {
    ThreadDemo3 td3; 

    @Override
    public void run() {
        System.out.println("线程B启动");
        td3.isFlag = true; 
        System.out.println("线程B执行完成~");
        System.out.println("t3.flag: " + td3.isFlag);
    }
}

// 启动线程A和线程B
public class Main {
    public static void main(String[] args) {
        ThreadDemo3 t3 = new ThreadDemo3(); // 线程A
        ThreadDemo4 t4 = new ThreadDemo4(); // 线程B
        t4.td3 = t3; // 让线程B持有线程A的引用

        // 先启动线程A
        t3.start();

        // 休眠1秒:确保线程A先进入循环
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        // 启动线程B
        t4.start();
    }
}

四、总结

  1. 线程安全问题根源:多线程操作共享数据时,JVM 主内存与线程工作内存的数据同步不一致,且非原子操作可被中断
  2. synchronized:通过加锁 / 解锁保证同步代码块的原子性,锁对象必须是所有线程共享的唯一对象
  3. volatile:保证变量的可见性,适合 “一个线程写、多个线程读” 的场景
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐