深入剖析多线程核心概念、线程安全问题的根源,以及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后优化为:

  1. 偏向锁:无竞争时,线程获取锁后标记为“偏向当前线程”,后续无需竞争;
  2. 轻量级锁:轻度竞争时,用CAS自旋获取锁,避免内核态切换;
  3. 重量级锁:重度竞争时,升级为操作系统互斥量(阻塞线程)。

2. Lock vs synchronized

特性 synchronized Lock(ReentrantLock)
锁释放 自动释放(异常也释放) 手动释放(必须放finally)
锁获取 阻塞式 可中断、可超时、非阻塞
公平锁 非公平 可公平/非公平
条件变量 单个(wait/notify) 多个(Condition)
性能 高并发下略差 高并发下更优

六、实战避坑指南

  1. 避免死锁
    • 按固定顺序获取锁;
    • 尝试超时获取锁(tryLock);
    • 避免锁嵌套过深。
  2. 减少锁竞争
    • 缩小同步代码块范围;
    • 用细粒度锁(如ConcurrentHashMap的分段锁);
    • 无锁方案(原子类、CAS)优先。
  3. volatile使用场景
    • 状态标记(开关);
    • 双重检查锁单例;
    • 禁止指令重排序。

总结

  1. 线程安全根源:可见性(缓存)、原子性(复合操作)、有序性(指令重排序);
  2. volatile:解决可见性+有序性,不解决原子性,适合单变量读写、状态标记;
  3. 同步机制:synchronized(JVM内置锁,简单易用)、Lock(显式锁,灵活)、原子类(CAS无锁,高性能);
  4. 核心原则:按需选择同步方案,优先缩小锁范围、使用无锁方案,避免死锁和过度竞争。
posted @ 2026-03-06 17:04  七星6609  阅读(10)  评论(0)    收藏  举报