news 2026/10/14 14:25:37

Java多线程:线程不安全产生的原因及解决方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java多线程:线程不安全产生的原因及解决方法

在 Java 并发编程中,线程安全是绕不开的核心问题。当多个线程同时执行代码,运行结果和单线程预期不一致,就发生线程不安全。本文结合底层原理,梳理线程不安全的根源,以及对应的解决手段synchronized, volatile, wait, notify, 设计模式等。

1.什么是线程不安全

  • 如果多线程环境下程序运行结果, 和单线程环境下预期结果不一致, 就代表线程不安全

经典示例:两个线程同时对同一个变量做 50000 次自增,预期最终结果 100000,实际运行结果往往小于 100000。

我们用代码举例试试看:

private static int count=0; public static void main(String[] args) throws InterruptedException{ Thread t1=new Thread(()->{ for (int i = 0; i < 50000; i++) { count++; } }); Thread t2=new Thread(()->{ for (int i = 0; i < 50000; i++) { count++; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count: "+count); }

运行结果:

很明显的看到两次运行的结果不同, 且都小于10000

注: count++ 看上去是一行java代码,底层是分为三个步骤:

  1. 从主内存读取到数据到CPU寄存器
  2. 在寄存器完成+1 运行
  3. 将计算结果写回主内存

线程抢占调度会打断这三步,多个线程同时读写同一个变量,就出现数据错乱。

2.线程不安全的四大根本原因

2.1 线程抢占式调度(根本前提)

操作系统对线程是抢占式调度,线程什么时候让出 CPU、什么时候被调度执行,是随机、不可预测的。 多个线程执行顺序千变万化,如果代码没有做并发保护,在某些调度时序下就会出错。

2.2 操作不具有原子性

原子性: 一段操作, 要么全部执行完毕, 要么完全不执行, 中间不能被其他线程打断

很多 Java 语句不是原子操作,例如 count++ 是 3 条 CPU 指令。线程执行一半被切走,其他线程介入修改数据,就产生错误。

类比:卖票系统,线程 A 读到还有 1 张票,还没完成票数更新,线程 B 同时读到还有 1 张票,两个线程都完成卖票,同一张票卖给两个人。

2.3 内存可见性问题

2.3.1 java内存模型JMM:
  • 主内存: 存放共享变量(对应物理内存)
  • 工作内存: 每个线程独有, 本质是CPU寄存器 高速缓存
2.3.2 线程修改共享变量流程:
  1. 将主内存变量拷贝副本到自己的工作内存
  2. 在工作内存修改副本
  3. 再把副本刷新回主内存

可见性问题: 一个线程修改了工作内存的副本, 没有及时刷新到主内存, 其他线程读取到主内存拿到的值还是旧值, 感知不到修改.

比如子线程循环读取 flag,主线程修改 flag,子线程永远看不到 flag 的变化,循环无法退出。

2.4指令重排序

为提升执行效率,编译器、CPU 会对指令做重排序优化,在保证单线程逻辑不变前提下,调整代码执行顺序。 单线程下重排序没有问题, 但多线程环境,重排序会打乱多线程之间依赖逻辑,引发 bug。

例如:单例模式instance = new Singleton(),底层分为分配内存、初始化对象、赋值引用三步,重排序可能出现先赋值引用,再初始化对象,其他线程拿到半初始化的对象。

总结: 触发线程安全的必要条件:多线程 + 共享变量 + 对变量进行修改。只读共享变量不会线程不安全。

3.线程不安全的解决方案

解决线程安全就是解决上面的四个问题:保证原子性, 保证可见性, 抑制不合理重排序, 同时规避抢占调度带来的执行顺序混乱.

3.1 方案一: synchronized 同步锁(监视器锁 MonitorLock)

synchronized 可以同时保证: 原子性, 可见性, 抑制指令重排序

核心是互斥: 同一时刻, 只有一个线程能拿到锁执行同步代码块, 其他线程阻塞等待锁释放

synchronized 三种用法

1. 修饰代码块, 指定锁对象

Object locker=new Object(); synchronized (locker){ count++; }

2.修饰普通方法: 锁当前对象 this

public synchronized void add(){ count++; }

3. 修饰静态方法: 锁类对象 (类的Class对象)

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

特性:

  1. 互斥:竞争同一把锁才会阻塞, 不同锁之间互不影响
  2. 可重入: 同一个线程可以重复获取同一把锁, 不会自己把自己锁死, 内部维护线程持有者+计数器

修改上面自增不安全的代码, 加上synchronized 就可以得到正确的10000结果:

private static int count=0; private static Object locker=new Object(); public static void main(String[] args) throws InterruptedException{ Thread t1=new Thread(()->{ for (int i = 0; i < 50000; i++) { synchronized (locker) { count++; } } }); Thread t2=new Thread(()->{ for (int i = 0; i < 50000; i++) { synchronized (locker) { count++; } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count: "+count); }

运行结果:

3.2 方案二: volatile关键字

volatile两大能力:

  1. 保证内存可见性:读写 volatile 变量强制和主内存交互,不使用线程本地缓存副本;写操作立刻刷新到主内存,读操作每次从主内存读取最新值。
  2. 禁止指令重排序。

⚠️ 重点:volatile 不能保证原子性!

适合场景:一个线程写,多个线程读,解决可见性。不适合场景:多线程同时写同一个变量(比如 count++,加 volatile 依旧线程不安全)。

例: 解决标志位可见性问题

static class Counter{ public volatile int flag=0; }

面试高频: 双重校验锁DCL懒汉单例, instance 必须加volatile, 防止指令重排序导致拿到半初始化对象.

class Singleton{ //静态共享变量,volatile是关键 private static volatile Singleton instance=null; //私有构造方法: 防止外部new对象 private Singleton(){} //对外获取实例的静态方法 public static Singleton getInstance(){ //第一层判断: 外层if,减少抢锁开销 if (instance==null){ //类锁,多个线程竞争这一把锁 synchronized (Singleton.class){ //第二次判断: 内层if,防止多线程等待锁之后重复创建对象 if (instance==null){ instance=new Singleton(); } } } return instance; } }

3.3 方案三: wait/notify 做线程之间的协调

synchronized解决竞;wait() / notify() / notifyAll()解决线程之间的执行顺序协调。

  • wait():释放锁,线程进入等待队列,等待被唤醒;必须写在 synchronized 同步块内。
  • notify():唤醒该对象等待队列中随机一个等待线程;不会立刻释放锁,退出同步块才释放锁。
  • notifyAll():唤醒该对象等待队列全部线程,唤醒后全部线程重新竞争锁。

注: wait 建议放在while 循环判断条件, 防止虚假唤醒. 典型使用场景: 阻塞队列, 生产者-消费者类型.

3.4 方案四: 规避共享可变数据(无锁思路)

  1. 不共享数据:每个线程使用局部变量(栈上变量,线程私有,不存在多线程竞争)。
  2. 共享只读数据:变量只读取,不修改,没有写操作,不会线程不安全,例如String不可变类。

3.5 方案五: 使用线程安全的类

Java 标准库区分线程安全 / 不安全集合:

  • ❌线程不安全:ArrayList、HashMap、HashSet、StringBuilder
  • ✅线程安全:StringBuffer、Vector、HashTable(不推荐,性能差);ConcurrentHashMap(并发推荐)

BlockingQueue阻塞队列,天生线程安全,生产者消费者模型首选。

3.6 方案六: 线程池

  • 复用线程,减少频繁创建销毁线程开销;同时管理多线程任务,配合阻塞队列做任务管理。

4.面试高频对比

4.1 synchronized vs volatile

synchronizedvolatile
原子性✅保证❌不保证
可见性✅保证✅保证
重排序✅禁止✅禁止
作用修饰代码块 / 方法,加锁互斥修饰变量
使用场景多线程读写共享变量一个线程写、多线程读,解决可见性

4.2 wait() vs sleep()

  1. wait()是 Object 方法;sleep()是 Thread 静态方法。
  2. wait()必须在synchronized中调用,会释放锁;sleep()不需要锁,不会释放锁。
  3. wait()用于线程间通信,等待其他线程 notify 唤醒;sleep()单纯休眠一段时间,时间到自动恢复。

5.总结

  1. 优先避免问题:尽量不使用共享可变对象;使用局部变量;使用不可变对象只读。
  2. 保证原子性:多线程修改共享变量,使用synchronized锁,保证操作不可被打断。
  3. 保证可见性:变量用volatile修饰,或者使用锁(synchronized 自带可见性)。
  4. 禁止指令重排序:volatile、synchronized都可以限制重排序。
  5. 协调线程执行顺序:wait‑notify、阻塞队列,做生产者消费者模型。

注意:锁会带来性能损耗,不要无脑到处加锁,只把真正存在竞争的临界区代码放进同步块,缩小锁粒度。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 14:25:07

MCP过时了么?你可能面对的MCP 反模式

【引】MCP 过时了&#xff1f;昙花一现还是会一直服务于系统&#xff1f;老码农觉得&#xff0c;很多时候是我们用错了。 对于MCP&#xff0c;简单而已&#xff0c;它就是让 AI Agent 能连上你们公司各种系统&#xff08;Salesforce、GitHub、Jira 这些&#xff09;的一套标准…

作者头像 李华
网站建设 2026/10/14 14:24:53

程序员薪资评估:用分位值破解平均值幻觉与薪资倒挂真相

前几天在一个技术社群里&#xff0c;有人抛出一句话&#xff1a;同样干了五年后端&#xff0c;为什么我拿到的月薪比同届进公司的同事少三分之一&#xff1f;帖子瞬间炸出一堆匿名工资条。评论区里最扎心的不是谁比谁低&#xff0c;而是很多人根本不知道自己的工资在市场上到底…

作者头像 李华
网站建设 2026/10/14 14:24:10

用C++解析CIM/XML:构建电力设备对象图的实现与避坑指南

简介&#xff1a;这是一份用C实现的CIM模型解析程序&#xff0c;面向电力系统数据交换、智能电网相关开发人员&#xff0c;解决CIM标准模型文件的读取与基本结构识别问题。程序基于MSXML组件解析XML数据&#xff0c;可识别设备、线路等实体及其属性关系&#xff0c;解析速度经过…

作者头像 李华
网站建设 2026/10/14 14:24:03

硬核科研出彩,日常体验翻车?Gemini 4 Argon 的“冰火两重天”

10 月 13 日&#xff0c;Google 面向开发者、企业、科研人员与消费者扩大开放 Gemini 4 系列的首发模型 Gemini 4 Argon。这款在 9 月底率先向安全合作伙伴亮相的旗舰模型&#xff0c;终于进入了更广泛的日常使用场景。 随之而来的&#xff0c;是两种反差鲜明的评价。 在官方…

作者头像 李华
网站建设 2026/10/14 14:24:00

C#实现Zebra打印机ZPL指令打印:从连接到批量稳定输出

简介&#xff1a;这份资源是面向C#开发者的Zebra打印机集成示例项目&#xff0c;适合需要在物流、零售、医疗等场景中实现标签、收据及条形码打印的初中级开发者参考。压缩包共88个文件&#xff0c;约348KB&#xff0c;以cs源码、sample示例、resx资源、config配置、csproj工程…

作者头像 李华
网站建设 2026/10/14 14:23:38

相机激光传感器 三维测量更精准

在现代工业质检与自动化生产线上&#xff0c;传统接触式测量、普通2D视觉检测的短板正在被快速放大&#xff1a;金属反光造成成像失准、复杂曲面无法获取高度维度数据、高速产线节拍下漏检误检率居高不下&#xff0c;这些长期困扰制造端的痛点&#xff0c;正在被相机激光传感器…

作者头像 李华