JAVA|多线程安全
·
前言
当多个线程同时操作同一个共享数据(尤其是读写交叉或修改)时,由于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();
}
}
四、总结
- 线程安全问题根源:多线程操作共享数据时,JVM 主内存与线程工作内存的数据同步不一致,且非原子操作可被中断
- synchronized:通过加锁 / 解锁保证同步代码块的原子性,锁对象必须是所有线程共享的唯一对象
- volatile:保证变量的可见性,适合 “一个线程写、多个线程读” 的场景
更多推荐



所有评论(0)