在 Java 并发编程中线程安全是绕不开的核心问题。当多个线程同时执行代码运行结果和单线程预期不一致就发生线程不安全。本文结合底层原理梳理线程不安全的根源以及对应的解决手段synchronized, volatile, wait, notify, 设计模式等。1.什么是线程不安全如果多线程环境下程序运行结果, 和单线程环境下预期结果不一致, 就代表线程不安全经典示例两个线程同时对同一个变量做 50000 次自增预期最终结果 100000实际运行结果往往小于 100000。我们用代码举例试试看:private static int count0; public static void main(String[] args) throws InterruptedException{ Thread t1new Thread(()-{ for (int i 0; i 50000; i) { count; } }); Thread t2new 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代码,底层是分为三个步骤:从主内存读取到数据到CPU寄存器在寄存器完成1 运行将计算结果写回主内存线程抢占调度会打断这三步多个线程同时读写同一个变量就出现数据错乱。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 线程修改共享变量流程:将主内存变量拷贝副本到自己的工作内存在工作内存修改副本再把副本刷新回主内存可见性问题: 一个线程修改了工作内存的副本, 没有及时刷新到主内存, 其他线程读取到主内存拿到的值还是旧值, 感知不到修改.比如子线程循环读取 flag主线程修改 flag子线程永远看不到 flag 的变化循环无法退出。2.4指令重排序为提升执行效率编译器、CPU 会对指令做重排序优化在保证单线程逻辑不变前提下调整代码执行顺序。 单线程下重排序没有问题, 但多线程环境重排序会打乱多线程之间依赖逻辑引发 bug。例如单例模式instance new Singleton()底层分为分配内存、初始化对象、赋值引用三步重排序可能出现先赋值引用再初始化对象其他线程拿到半初始化的对象。总结: 触发线程安全的必要条件多线程 共享变量 对变量进行修改。只读共享变量不会线程不安全。3.线程不安全的解决方案解决线程安全就是解决上面的四个问题:保证原子性, 保证可见性, 抑制不合理重排序, 同时规避抢占调度带来的执行顺序混乱.3.1 方案一: synchronized 同步锁(监视器锁 MonitorLock)synchronized 可以同时保证: 原子性, 可见性, 抑制指令重排序核心是互斥: 同一时刻, 只有一个线程能拿到锁执行同步代码块, 其他线程阻塞等待锁释放synchronized 三种用法1. 修饰代码块, 指定锁对象Object lockernew Object(); synchronized (locker){ count; }2.修饰普通方法: 锁当前对象 thispublic synchronized void add(){ count; }3. 修饰静态方法: 锁类对象 (类的Class对象)public static synchronized void add(){ count; }特性:互斥:竞争同一把锁才会阻塞, 不同锁之间互不影响可重入: 同一个线程可以重复获取同一把锁, 不会自己把自己锁死, 内部维护线程持有者计数器修改上面自增不安全的代码, 加上synchronized 就可以得到正确的10000结果:private static int count0; private static Object lockernew Object(); public static void main(String[] args) throws InterruptedException{ Thread t1new Thread(()-{ for (int i 0; i 50000; i) { synchronized (locker) { count; } } }); Thread t2new 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两大能力保证内存可见性读写 volatile 变量强制和主内存交互不使用线程本地缓存副本写操作立刻刷新到主内存读操作每次从主内存读取最新值。禁止指令重排序。⚠️ 重点volatile 不能保证原子性适合场景一个线程写多个线程读解决可见性。不适合场景多线程同时写同一个变量比如 count加 volatile 依旧线程不安全。例: 解决标志位可见性问题static class Counter{ public volatile int flag0; }面试高频: 双重校验锁DCL懒汉单例, instance 必须加volatile, 防止指令重排序导致拿到半初始化对象.class Singleton{ //静态共享变量,volatile是关键 private static volatile Singleton instancenull; //私有构造方法: 防止外部new对象 private Singleton(){} //对外获取实例的静态方法 public static Singleton getInstance(){ //第一层判断: 外层if,减少抢锁开销 if (instancenull){ //类锁,多个线程竞争这一把锁 synchronized (Singleton.class){ //第二次判断: 内层if,防止多线程等待锁之后重复创建对象 if (instancenull){ instancenew Singleton(); } } } return instance; } }3.3 方案三: wait/notify 做线程之间的协调synchronized解决竞wait() / notify() / notifyAll()解决线程之间的执行顺序协调。wait()释放锁线程进入等待队列等待被唤醒必须写在 synchronized 同步块内。notify()唤醒该对象等待队列中随机一个等待线程不会立刻释放锁退出同步块才释放锁。notifyAll()唤醒该对象等待队列全部线程唤醒后全部线程重新竞争锁。注: wait 建议放在while 循环判断条件, 防止虚假唤醒. 典型使用场景: 阻塞队列, 生产者-消费者类型.3.4 方案四: 规避共享可变数据(无锁思路)不共享数据每个线程使用局部变量栈上变量线程私有不存在多线程竞争。共享只读数据变量只读取不修改没有写操作不会线程不安全例如String不可变类。3.5 方案五: 使用线程安全的类Java 标准库区分线程安全 / 不安全集合❌线程不安全ArrayList、HashMap、HashSet、StringBuilder✅线程安全StringBuffer、Vector、HashTable不推荐性能差ConcurrentHashMap并发推荐BlockingQueue阻塞队列天生线程安全生产者消费者模型首选。3.6 方案六: 线程池复用线程减少频繁创建销毁线程开销同时管理多线程任务配合阻塞队列做任务管理。4.面试高频对比4.1 synchronized vs volatilesynchronizedvolatile原子性✅保证❌不保证可见性✅保证✅保证重排序✅禁止✅禁止作用修饰代码块 / 方法加锁互斥修饰变量使用场景多线程读写共享变量一个线程写、多线程读解决可见性4.2 wait() vs sleep()wait()是 Object 方法sleep()是 Thread 静态方法。wait()必须在synchronized中调用会释放锁sleep()不需要锁不会释放锁。wait()用于线程间通信等待其他线程 notify 唤醒sleep()单纯休眠一段时间时间到自动恢复。5.总结优先避免问题尽量不使用共享可变对象使用局部变量使用不可变对象只读。保证原子性多线程修改共享变量使用synchronized锁保证操作不可被打断。保证可见性变量用volatile修饰或者使用锁synchronized 自带可见性。禁止指令重排序volatile、synchronized都可以限制重排序。协调线程执行顺序wait‑notify、阻塞队列做生产者消费者模型。注意锁会带来性能损耗不要无脑到处加锁只把真正存在竞争的临界区代码放进同步块缩小锁粒度。