深入剖析多线程核心概念、线程安全问题的根源,以及volatile关键字、线程同步机制和锁的原理与实战应用
深入剖析多线程核心:概念、问题、同步与锁(原理+实战)
本文会从底层根源到实战落地,系统拆解多线程的核心逻辑——先讲清楚线程安全为什么会出现,再逐一剖析 volatile、同步机制、锁的原理与实际用法,全程结合代码示例,新手也能看懂。
一、多线程核心概念(先打基础)
1. 什么是线程?
线程是CPU调度的最小单位,进程是资源分配的最小单位(一个进程可包含多个线程,共享进程的内存、文件句柄等资源)。
- 单线程:代码串行执行,只有一个执行流;
- 多线程:多个执行流并行(宏观)/并发(微观)执行,目的是利用多核CPU、提高程序吞吐量。
2. 多线程的核心问题
多线程的所有坑,本质都源于:
- 线程间共享资源(多个线程操作同一块内存数据);
- CPU的线程切换(CPU以时间片轮转方式切换线程,执行顺序不可控)。
二、线程安全问题的根源(3大核心原因)
线程安全 = 多线程环境下,程序执行结果和单线程一致。反之,结果混乱就是线程不安全,根源有3个:
1. 可见性问题
定义:线程A修改了共享变量,线程B无法立刻看到最新值。
底层原因:CPU缓存架构(每个核心有L1/L2缓存,变量会被缓存到核心私有缓存,而非直接写回主存)。
示例:
// 可见性问题演示:不加volatile,线程2永远循环
public class VisibilityDemo {
private boolean flag = false; // 无volatile
public void writer() {
flag = true; // 线程1修改flag
}
public void reader() {
while (!flag) { // 线程2读取flag,一直读缓存的false
// 空循环
}
System.out.println("线程2退出");
}
public static void main(String[] args) {
VisibilityDemo demo = new VisibilityDemo();
new Thread(demo::reader).start();
try { Thread.sleep(100); } catch (InterruptedException e) {}
new Thread(demo::writer).start();
}
}
结果:线程2永远不会退出(看不到flag=true)。
2. 原子性问题
定义:一个操作(或多个操作)要么全部执行完成,要么都不执行,中间不会被线程切换打断。
底层原因:复合操作被拆分为多个CPU指令,线程切换会导致指令执行不完整。
示例:
// 原子性问题演示:count++不是原子操作
public class AtomicityDemo {
private int count = 0;
public void increment() {
count++; // 拆分为:读count → 加1 → 写count
}
public static void main(String[] args) throws InterruptedException {
AtomicityDemo demo = new AtomicityDemo();
// 1000个线程,每个执行1000次count++
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
}).start();
}
Thread.sleep(2000); // 等待所有线程执行完
System.out.println("最终count值:" + demo.count); // 结果远小于1000000
}
}
结果:count最终值远小于1000000(多个线程同时执行“读-改-写”,覆盖彼此的结果)。
3. 有序性问题
定义:程序执行顺序和代码编写顺序一致。
底层原因:JVM/CPU为了优化性能,会对指令进行重排序(单线程语义不变,多线程混乱)。
示例:
// 有序性问题演示:指令重排序导致初始化未完成就被使用
public class OrderingDemo {
private static int a = 0;
private static boolean init = false;
// 线程1:初始化数据
public static void writer() {
a = 1; // 步骤1
init = true; // 步骤2(可能被重排序到步骤1前面)
}
// 线程2:使用数据
public static void reader() {
if (init) { // 如果init=true,认为a已经初始化
System.out.println(a); // 可能输出0(因为步骤2先执行,步骤1后执行)
}
}
public static void main(String[] args) {
new Thread(OrderingDemo::writer).start();
new Thread(OrderingDemo::reader).start();
}
}
结果:可能输出0(而非预期的1),因为JVM把init=true重排序到a=1前面。
三、volatile关键字(轻量级同步)
volatile是Java解决可见性、有序性的轻量级方案(无锁,性能优于synchronized),但不解决原子性。
1. volatile核心原理
- 可见性:volatile变量的写操作会触发“写屏障”,强制将变量刷回主存;读操作触发“读屏障”,强制从主存读取最新值(而非缓存)。
- 有序性:通过内存屏障禁止指令重排序(volatile写前插StoreStore、后插StoreLoad;volatile读前插LoadLoad、后插LoadStore)。
- 原子性:仅保证单个volatile变量的读写原子性,复合操作(如count++)仍不安全。
2. volatile实战场景
场景1:状态标记(一写多读)
// 解决可见性+有序性:用volatile修饰状态标记
public class VolatileFlagDemo {
private volatile boolean flag = false; // 加volatile
public void writer() {
flag = true; // 写屏障:刷回主存
}
public void reader() {
while (!flag) { // 读屏障:从主存读
}
System.out.println("线程2退出");
}
public static void main(String[] args) {
VolatileFlagDemo demo = new VolatileFlagDemo();
new Thread(demo::reader).start();
try { Thread.sleep(100); } catch (InterruptedException e) {}
new Thread(demo::writer).start();
}
}
结果:线程2能立刻看到flag=true,退出循环。
场景2:禁止指令重排序(双重检查锁单例)
// 正确的双重检查锁单例(必须加volatile)
public class Singleton {
private static volatile Singleton instance; // 禁止指令重排序
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁,提高效率)
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次检查(防止多线程同时进入第一层)
instance = new Singleton(); // 不加volatile会重排序:分配内存→赋值引用→初始化对象
}
}
}
return instance;
}
}
关键:volatile禁止instance = new Singleton()的指令重排序,避免其他线程拿到“未初始化完成”的对象。
3. volatile的局限性
// volatile不解决原子性问题
public class VolatileAtomicDemo {
private volatile int count = 0;
public void increment() {
count++; // 复合操作:读→加1→写,volatile无法保证原子性
}
public static void main(String[] args) throws InterruptedException {
VolatileAtomicDemo demo = new VolatileAtomicDemo();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
}).start();
}
Thread.sleep(2000);
System.out.println("count=" + demo.count); // 仍小于1000000
}
}
结论:volatile只适合“单个变量的读写”,复合操作需用锁或原子类。
四、线程同步机制(解决原子性)
同步机制的核心是保证临界区代码的原子性(同一时间只有一个线程执行),常见方案有:
1. synchronized(内置锁)
原理
synchronized是JVM层面的互斥锁,基于对象的监视器锁(monitor) 实现:
- 进入synchronized代码块:获取monitor(锁),没拿到则阻塞;
- 退出synchronized代码块:释放monitor,唤醒等待的线程;
- 特性:可重入(同一线程多次获取同一把锁不会死锁)、非公平(不保证先等先得)。
实战用法
用法1:修饰实例方法(锁当前对象)
public class SyncInstanceMethod {
private int count = 0;
// 锁:this(当前实例对象)
public synchronized void increment() {
count++; // 临界区:原子执行
}
public static void main(String[] args) throws InterruptedException {
SyncInstanceMethod demo = new SyncInstanceMethod();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
}).start();
}
Thread.sleep(2000);
System.out.println("count=" + demo.count); // 等于1000000
}
}
用法2:修饰静态方法(锁类对象)
public class SyncStaticMethod {
private static int count = 0;
// 锁:SyncStaticMethod.class(类对象)
public static synchronized void increment() {
count++;
}
}
用法3:修饰代码块(锁任意对象)
public class SyncBlock {
private int count = 0;
private final Object lock = new Object(); // 自定义锁对象(推荐)
public void increment() {
synchronized (lock) { // 锁:lock对象
count++;
}
}
}
注意事项
- 锁对象必须是不可变对象(如new Object()),避免锁对象被重新赋值导致锁失效;
- 尽量缩小同步代码块范围(只包临界区),减少锁竞争,提高性能。
2. Lock接口(显式锁,JUC)
java.util.concurrent.locks.Lock是JDK层面的锁,比synchronized更灵活(可中断、可超时、公平锁)。
核心实现:ReentrantLock(可重入锁)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private int count = 0;
private final Lock lock = new ReentrantLock(); // 默认非公平锁,可传true设为公平锁
public void increment() {
lock.lock(); // 获取锁(阻塞)
try {
count++; // 临界区
} finally {
lock.unlock(); // 释放锁(必须放finally,防止异常导致锁不释放)
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
}).start();
}
Thread.sleep(2000);
System.out.println("count=" + demo.count); // 等于1000000
}
}
Lock的高级特性
特性1:可中断锁
// 可中断获取锁:线程等待锁时,可被其他线程中断(避免死等)
lock.lockInterruptibly(); // 需捕获InterruptedException
特性2:超时获取锁
// 尝试获取锁,5秒内没拿到则返回false
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 临界区
} finally {
lock.unlock();
}
} else {
// 获取锁失败,做降级处理
}
特性3:公平锁
// 公平锁:先等待的线程先拿到锁(性能略低,避免饥饿)
Lock fairLock = new ReentrantLock(true);
3. 原子类(无锁方案,CAS)
原理
原子类(AtomicInteger、AtomicLong等)基于CAS(Compare And Swap) 实现:
- CAS三步:比较(当前值是否等于预期值)→ 交换(如果相等则更新为新值)→ 返回(是否成功);
- 底层:CPU指令
cmpxchg(原子操作),无锁,性能优于synchronized。
实战:解决count++原子性问题
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerDemo {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // CAS实现原子自增
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo demo = new AtomicIntegerDemo();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
}).start();
}
Thread.sleep(2000);
System.out.println("count=" + demo.count.get()); // 等于1000000
}
}
CAS的局限性
- ABA问题:变量从A→B→A,CAS会认为没变化(解决:AtomicStampedReference,加版本号);
- 自旋开销:高并发下CAS失败会一直自旋,消耗CPU(解决:LongAdder,分段锁)。
五、锁的底层原理(进阶)
1. synchronized的优化(JDK1.6+)
synchronized早期是“重量级锁”(依赖操作系统互斥量),JDK1.6后优化为:
- 偏向锁:无竞争时,线程获取锁后标记为“偏向当前线程”,后续无需竞争;
- 轻量级锁:轻度竞争时,用CAS自旋获取锁,避免内核态切换;
- 重量级锁:重度竞争时,升级为操作系统互斥量(阻塞线程)。
2. Lock vs synchronized
| 特性 | synchronized | Lock(ReentrantLock) |
|---|---|---|
| 锁释放 | 自动释放(异常也释放) | 手动释放(必须放finally) |
| 锁获取 | 阻塞式 | 可中断、可超时、非阻塞 |
| 公平锁 | 非公平 | 可公平/非公平 |
| 条件变量 | 单个(wait/notify) | 多个(Condition) |
| 性能 | 高并发下略差 | 高并发下更优 |
六、实战避坑指南
- 避免死锁:
- 按固定顺序获取锁;
- 尝试超时获取锁(tryLock);
- 避免锁嵌套过深。
- 减少锁竞争:
- 缩小同步代码块范围;
- 用细粒度锁(如ConcurrentHashMap的分段锁);
- 无锁方案(原子类、CAS)优先。
- volatile使用场景:
- 状态标记(开关);
- 双重检查锁单例;
- 禁止指令重排序。
总结
- 线程安全根源:可见性(缓存)、原子性(复合操作)、有序性(指令重排序);
- volatile:解决可见性+有序性,不解决原子性,适合单变量读写、状态标记;
- 同步机制:synchronized(JVM内置锁,简单易用)、Lock(显式锁,灵活)、原子类(CAS无锁,高性能);
- 核心原则:按需选择同步方案,优先缩小锁范围、使用无锁方案,避免死锁和过度竞争。

浙公网安备 33010602011771号