一、线程基础

1. 线程的四种创建方式及优缺点对比

Java 中创建线程有四种核心方式,底层均依赖 Thread 类的 run() 方法,只是实现形式不同,具体对比如下:

创建方式 实现方式 优点 缺点
继承 Thread 类 1. 自定义类继承 Thread;2. 重写 run() 方法;3. 创建实例并调用 start() 1. 实现简单,代码直观;2. 可直接通过 this 访问线程对象。 1. Java 单继承限制,无法再继承其他类;2. 业务逻辑与线程耦合,不利于解耦;3. 无法获取返回值。
实现 Runnable 接口 1. 自定义类实现 Runnable;2. 实现 run() 方法;3. 将实例传入 Thread 构造器,调用 start() 1. 规避单继承限制,可继承其他类;2. 业务逻辑与线程解耦,符合 “单一职责”;3. 可共享 Runnable 实例(多线程共享数据)。 1. 无法直接获取返回值;2. 需借助 Thread 类启动线程,稍繁琐。
实现 Callable 接口 1. 自定义类实现 Callable<V>;2. 实现 call() 方法(可抛异常、有返回值);3. 封装为 FutureTask,传入 Thread 启动;4. 通过 FutureTask.get() 获取返回值。 1. 有返回值(解决前两种无返回值问题);2. 可抛出受检异常;3. 同样规避单继承限制。 1. get() 方法会阻塞线程(直到获取返回值);2. 代码实现相对复杂。
线程池创建 1. 通过 ExecutorsThreadPoolExecutor 创建线程池;2. 提交 Runnable/Callable 任务;3. 线程池管理线程生命周期。 1. 复用线程,避免频繁创建 / 销毁线程的开销;2. 可控制并发数,防止资源耗尽;3. 提供任务队列、拒绝策略等扩展能力;4. 支持批量提交任务、获取返回值。 1. 需手动管理线程池(如关闭),否则可能导致内存泄漏;2. 入门门槛稍高,需理解核心参数(核心线程数、最大线程数等)。

代码示例(核心方式)

// 1. 继承Thread
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("继承Thread创建线程");
    }
}

// 2. 实现Runnable
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("实现Runnable创建线程");
    }
}

// 3. 实现Callable
class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Callable返回值";
    }
}

// 4. 线程池
public class ThreadCreateDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 方式1
        new MyThread().start();
        
        // 方式2
        new Thread(new MyRunnable()).start();
        
        // 方式3
        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        new Thread(futureTask).start();
        System.out.println(futureTask.get()); // 获取返回值
        
        // 方式4
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.submit(new MyRunnable());
        Future<String> future = executor.submit(new MyCallable());
        System.out.println(future.get());
        executor.shutdown(); // 关闭线程池
    }
}
2. 线程六大状态及状态转换

JDK 1.5 后,Thread.State 枚举定义了线程的六大状态,状态转换由具体的线程操作触发,核心关系如下:

(1)六大状态定义
状态名称 含义
NEW 线程已创建(调用 new Thread()),但未调用 start(),此时线程未启动。
RUNNABLE 线程调用 start() 后进入该状态(包含 “就绪” 和 “运行中”):- 就绪:线程已准备好,等待 CPU 调度;- 运行中:CPU 正在执行该线程。
BLOCKED 线程因竞争 synchronized 锁失败(被其他线程持有)而阻塞,直到获取锁。
WAITING 线程进入 “无限期等待” 状态,需被其他线程显式唤醒,否则一直等待。
TIMED_WAITING 线程进入 “有限期等待” 状态,超时后会自动唤醒,无需其他线程干预。
TERMINATED 线程执行完成(run()/call() 执行结束)或异常终止,生命周期结束。
(2)状态转换条件及触发场景
graph TD
    A[NEW] -->|调用start()| B[RUNNABLE]
    B -->|竞争synchronized锁失败| C[BLOCKED]
    C -->|获取synchronized锁| B
    B -->|调用wait()| D[WAITING]
    B -->|调用sleep(long)/wait(long)/join(long)| E[TIMED_WAITING]
    D -->|其他线程调用notify()/notifyAll()| B
    E -->|超时/其他线程调用notify()/notifyAll()| B
    B -->|run()/call()执行完成/异常终止| F[TERMINATED]
    D -->|其他线程调用interrupt()| B
    E -->|其他线程调用interrupt()| B

关键转换场景详解

  1. NEW → RUNNABLE:仅当调用 thread.start() 时触发(注意:start() 只能调用一次,重复调用会抛 IllegalThreadStateException);
  2. RUNNABLE → BLOCKED:线程进入 synchronized 代码块 / 方法,且锁被其他线程持有;
  3. BLOCKED → RUNNABLE:持有锁的线程释放锁(退出同步块),当前线程竞争到锁;
  4. RUNNABLE → WAITING
    • 调用 Object.wait()(无参):需先持有对象锁,调用后释放锁,进入等待队列;
    • 调用 Thread.join()(无参):等待目标线程执行完成;
    • 调用 LockSupport.park():无锁要求,直接进入等待。
  5. RUNNABLE → TIMED_WAITING
    • 调用 Thread.sleep(long ms):不释放锁,超时自动唤醒;
    • 调用 Object.wait(long ms):释放锁,超时或被唤醒;
    • 调用 Thread.join(long ms):等待目标线程指定时间;
    • 调用 LockSupport.parkNanos()/parkUntil():限时等待。
  6. WAITING/TIMED_WAITING → RUNNABLE
    • WAITING:其他线程调用 notify()/notifyAll()(需持有同一把锁),或调用 interrupt() 中断;
    • TIMED_WAITING:超时自动唤醒,或被 notify()/notifyAll()/interrupt() 触发。
  7. RUNNABLE → TERMINATED
    • 正常结束:run()/call() 执行完毕;
    • 异常结束:执行过程中抛出未捕获的异常(如 NullPointerException)。
3. 线程优先级、守护线程的特性与使用
(1)线程优先级(Thread Priority)
  • 核心特性

    1. 优先级范围:1(Thread.MIN_PRIORITY)~ 10(Thread.MAX_PRIORITY),默认优先级为 5(Thread.NORM_PRIORITY);
    2. 优先级是 “提示性” 的:JVM 或操作系统不一定严格按优先级调度,仅表示线程获取 CPU 时间片的概率高低(高优先级线程更可能被调度,但不绝对);
    3. 继承性:子线程的优先级默认与创建它的父线程一致。
  • 使用方式

    Thread thread = new Thread(new MyRunnable());
    thread.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级
    thread.start();
    System.out.println(thread.getPriority()); // 获取优先级
    
  • 注意事项

    • 不要依赖优先级实现核心业务逻辑(如 “高优先级线程必须先执行”),不同操作系统的调度策略不同;
    • 避免设置极端优先级(1 或 10),可能导致低优先级线程长期无法执行(饥饿问题)。
(2)守护线程(Daemon Thread)
  • 核心特性

    1. 定义:守护线程是 “后台线程”,为用户线程(非守护线程)提供服务(如 GC 线程、定时器线程);
    2. 生命周期依赖:当所有用户线程执行完毕,JVM 会自动退出,无论守护线程是否执行完成;
    3. 设置时机:必须在 start() 前调用 setDaemon(true),否则抛 IllegalThreadStateException
    4. 继承性:默认情况下,子线程的守护属性与父线程一致(父线程是守护线程,子线程默认也是)。
  • 使用方式

    Thread daemonThread = new Thread(() -> {
        while (true) { // 无限循环,但若所有用户线程结束,该线程会被终止
            System.out.println("守护线程运行中");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    daemonThread.setDaemon(true); // 设置为守护线程
    daemonThread.start();
    
    // 用户线程:执行3秒后结束
    Thread userThread = new Thread(() -> {
        for (int i = 0; i < 3; i++) {
            System.out.println("用户线程运行中");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    userThread.start();
    

    执行结果:用户线程执行 3 次后结束,JVM 退出,守护线程的无限循环被终止。

  • 注意事项

    • 守护线程中不要执行关键业务(如文件写入、数据保存),可能因 JVM 退出导致操作中断;
    • 守护线程的优先级默认较低,且无法通过 join() 等待其执行完成(因为用户线程结束后它会被强制终止)。

二、总结

  1. 线程四种创建方式中,线程池 是生产环境首选(高效、可控),Callable 适合需要返回值的场景,Runnable 解耦性优于 Thread 继承方式;
  2. 线程六大状态的核心转换触发点:start() 启动线程、synchronized 锁竞争触发 BLOCKED、wait()/sleep()/join() 触发等待状态、执行完成 / 异常触发 TERMINATED;
  3. 线程优先级是 “提示性” 调度策略,不可依赖;守护线程为用户线程服务,生命周期随用户线程结束而终止,适合做后台辅助任务。

二.synchronized核心知识

1. synchronized的底层实现:对象监视器(Monitor)

(1)Java 对象头与锁标记

每个 Java 对象在内存中包含三部分:对象头、实例数据、对齐填充。其中 对象头 是实现锁的关键,它由两部分组成(以 64 位 JVM 为例):

  • Mark Word(标记字段):占 64bit,存储对象的哈希值、GC 分代年龄、锁状态标记(无锁 / 偏向锁 / 轻量级锁 / 重量级锁)、持有锁的线程 ID 等核心信息;
  • Klass Pointer(类型指针):占 64bit,指向对象所属类的元数据。
(2)Monitor 对象(监视器)

Monitor 是一个底层的 C++ 对象(ObjectMonitor),每个 Java 对象都可以关联一个 Monitor(当对象被用作锁时,JVM 会为其创建 / 关联 Monitor)。ObjectMonitor 核心结构包括:

  • _owner:指向持有当前 Monitor 的线程;
  • _EntryList:等待获取锁的线程队列(阻塞状态);
  • _WaitSet:调用 wait() 后等待被唤醒的线程队列;
  • _count:锁的重入次数。
(3)底层执行逻辑
  • 当线程进入 synchronized 代码块时,JVM 会通过 monitorenter 指令尝试获取对象的 Monitor:
    • 如果 _owner 为空,当前线程会占用 _owner,并将 _count 置为 1,成功获取锁;
    • 如果当前线程已持有该 Monitor(重入),则 _count 加 1;
    • 如果 Monitor 被其他线程持有,当前线程会进入 _EntryList 阻塞,直到锁被释放。
  • 当线程退出 synchronized 代码块时,执行 monitorexit 指令:
    • _count 减 1,当 _count 为 0 时,释放 Monitor(_owner 置空),并唤醒 _EntryList 中的线程竞争锁。

补充:synchronized 修饰方法时,底层不依赖 monitorenter/monitorexit 指令,而是通过方法的 access_flags 标记(ACC_SYNCHRONIZED)实现,JVM 调用方法时会检查该标记,进而触发 Monitor 的获取 / 释放逻辑。

2. synchronized锁升级流程(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)

JDK 1.6 为了优化 synchronized 的性能,引入了 锁升级 机制(不可逆,只能升级不能降级),核心思想是 “按需升级”,避免一开始就使用性能开销大的重量级锁:

(1)无锁状态
  • 对象刚创建时,Mark Word 中锁标记为 “无锁”,存储对象哈希值、GC 年龄等信息,此时没有任何线程竞争锁。
(2)偏向锁(Biased Locking)
  • 适用场景:单线程重复获取同一把锁(无多线程竞争)。
  • 核心逻辑
    1. 当第一个线程获取锁时,JVM 会将 Mark Word 中的锁标记改为 “偏向锁”,并记录该线程的 ID(无需 CAS 操作,仅修改标记);
    2. 后续该线程再次进入 / 退出同步块时,无需竞争锁,只需检查 Mark Word 中的线程 ID 是否为自己,直接进入,大幅降低锁获取开销。
  • 撤销条件:当有第二个线程尝试获取锁时,偏向锁会被撤销(全局安全点 Safepoint 执行),根据当前线程状态升级为轻量级锁或重量级锁。
(3)轻量级锁(Lightweight Locking)
  • 适用场景:多线程交替获取锁(无持续竞争,即 “自旋” 可解决)。
  • 核心逻辑
    1. 线程获取锁时,JVM 会在该线程的栈帧中创建一块 “锁记录(Lock Record)” 区域,存储对象 Mark Word 的拷贝;
    2. 通过 CAS 操作将对象 Mark Word 改为指向该锁记录的指针,并将锁标记改为 “轻量级锁”,成功则获取锁;
    3. 如果 CAS 失败(说明有其他线程竞争),当前线程会通过 自旋(Spin) (循环重试 CAS)尝试获取锁(默认自旋 10 次),自旋成功仍可获取轻量级锁。
  • 升级条件:自旋达到阈值仍未获取锁,或自旋期间有更多线程竞争,轻量级锁升级为重量级锁。
(4)重量级锁(Heavyweight Locking)
  • 适用场景:多线程持续竞争锁(自旋无法解决)。
  • 核心逻辑
    1. 锁升级为重量级锁后,对象 Mark Word 会指向 Monitor 对象的地址,锁标记改为 “重量级锁”;
    2. 竞争失败的线程不再自旋,而是进入 Monitor 的 _EntryList 阻塞(用户态→内核态切换,开销大);
    3. 锁释放时唤醒阻塞线程,重新竞争锁。

锁升级流程总结:无锁 → (单线程获取)偏向锁 → (多线程交替竞争)轻量级锁(自旋) → (多线程持续竞争)重量级锁。

3. JVM 对 synchronized 的优化:锁消除、锁粗化

JDK 1.6 除了锁升级,还提供了 锁消除锁粗化 两种编译期 / 运行期优化,进一步降低 synchronized 的开销:

(1)锁消除(Lock Elimination)
  • 定义:JVM 编译器在编译阶段,检测到某些锁对象不存在多线程竞争的可能,直接消除该锁的获取 / 释放逻辑。
  • 核心原理:基于 “逃逸分析”(Escape Analysis)—— 判断对象是否仅在当前线程内使用(未逃逸到其他线程),若未逃逸,则该对象的锁无竞争意义。
  • 示例
    public String concat(String a, String b) {
        // StringBuffer 本身是线程安全的,内部方法有 synchronized 修饰
        StringBuffer sb = new StringBuffer();
        sb.append(a).append(b);
        return sb.toString();
    }
    
    JVM 经逃逸分析发现 sb 仅在 concat 方法内创建和使用,未逃逸到外部,会消除 sb.append() 中的 synchronized 锁,提升性能。
(2)锁粗化(Lock Coarsening)
  • 定义:JVM 将多个连续的、对同一把锁的获取 / 释放操作,合并为一次获取 / 释放,减少锁的频繁切换开销。
  • 适用场景:循环中频繁加锁 / 解锁(如 for 循环内的 synchronized 块)。
  • 示例
    // 优化前:循环内每次迭代都加锁、解锁
    for (int i = 0; i < 1000; i++) {
        synchronized (lock) {
            list.add(i);
        }
    }
    
    // JVM 锁粗化后:仅在循环外加一次锁、解锁
    synchronized (lock) {
        for (int i = 0; i < 1000; i++) {
            list.add(i);
        }
    }
    
    锁粗化避免了 1000 次锁的获取 / 释放操作,大幅降低开销。

二、总结

  1. synchronized 底层依赖 对象监视器(Monitor) 实现,核心关联 Java 对象头(Mark Word)和 monitorenter/monitorexit 指令(方法级同步依赖 ACC_SYNCHRONIZED 标记);
  2. 锁升级是 JDK 1.6 的核心优化,遵循 “无锁→偏向锁→轻量级锁→重量级锁” 的不可逆流程,按需升级以降低锁开销;
  3. JVM 还通过 锁消除(基于逃逸分析消除无竞争锁)和 锁粗化(合并连续锁操作)进一步优化 synchronized 的性能。

二.volatile核心原理

1. volatile 的核心特性及底层原理(内存可见性、禁止指令重排)

volatile 是 Java 提供的轻量级同步机制,核心作用是保证变量的内存可见性禁止指令重排,其底层完全依赖 CPU 的 内存屏障(Memory Barrier) 实现。

(1)内存可见性原理
  • 问题背景:多线程环境下,每个线程有自己的工作内存(CPU 缓存),线程读取变量时会先从主内存加载到工作内存,修改后仅更新工作内存,不会立即同步到主内存;其他线程无法感知该变量的修改,导致 “内存不可见”。

  • volatile 解决逻辑

    1. 当线程修改 volatile 变量时,JVM 会触发 写屏障(Store Barrier):强制将工作内存中的变量值刷新到主内存,而非仅停留在 CPU 缓存;
    2. 当线程读取 volatile 变量时,JVM 会触发 读屏障(Load Barrier):强制清空工作内存中该变量的缓存,从主内存重新加载最新值;
    3. 底层通过 CPU 的 sfence(写屏障)、lfence(读屏障)或 mfence(全屏障)指令实现,确保变量的读写直接操作主内存。
  • 示例理解

    // 线程A
    volatile boolean flag = false;
    public void setFlag() {
        flag = true; // 写屏障:立即刷到主内存
    }
    
    // 线程B
    public void loop() {
        while (!flag) { // 读屏障:每次都从主内存读最新值
            // 无操作
        }
        System.out.println("flag已更新");
    }
    

    flag 不加 volatile,线程 B 可能一直读取工作内存中的旧值(false),陷入死循环;加 volatile 后,线程 B 能立即感知线程 A 的修改。

(2)禁止指令重排原理
  • 问题背景:JVM 编译器和 CPU 为了优化执行效率,会对无依赖关系的指令进行 “重排序”(如:int a=1; int b=2; 可能被重排为 int b=2; int a=1;),但多线程下重排可能导致逻辑错误(如双重检查锁的单例问题)。

  • volatile 解决逻辑:JVM 为 volatile 变量的读写操作插入 内存屏障,限制指令重排的范围:

    1. volatile 变量写操作前:插入 StoreStore 屏障,禁止写操作之前的普通指令重排到写操作之后;
    2. volatile 变量写操作后:插入 StoreLoad 屏障,禁止写操作之后的指令重排到写操作之前;
    3. volatile 变量读操作前:插入 LoadLoad 屏障,禁止读操作之前的普通指令重排到读操作之后;
    4. volatile 变量读操作后:插入 LoadStore 屏障,禁止读操作之后的指令重排到读操作之前。
  • 经典场景:双重检查锁(DCL)的单例模式

    // 正确的DCL单例(instance必须加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;
        }
    }
    

    解释:instance = new Singleton() 实际分 3 步:① 分配内存 ② 初始化对象 ③ 指向内存地址。不加 volatile 时,JVM 可能重排为 ①→③→②,导致其他线程拿到 “未初始化完成” 的实例;加 volatile 后,禁止该重排,确保实例完全初始化后才被其他线程可见。

2. volatile 与 happens-before 原则的关联

happens-before(先行发生)是 Java 内存模型(JMM)的核心原则,定义了多线程操作的可见性规则,volatile 直接关联其中两条关键规则:

(1)volatile 变量规则
  • 规则内容:对一个 volatile 变量的写操作,happens-before 于后续对该变量的读操作
  • 通俗理解:线程 A 修改了 volatile 变量,线程 B 后续读取该变量时,一定能看到线程 A 的修改结果(这也是内存可见性的底层规则支撑)。
(2)传递性规则
  • 规则内容:如果 A happens-before B,B happens-before C,那么 A happens-before C。
  • 与 volatile 结合示例
    // 线程A执行:
    int a = 1;        // 操作1
    volatile int b = 2; // 操作2(volatile写)
    
    // 线程B执行:
    int c = b;        // 操作3(volatile读)
    int d = a;        // 操作4
    
    根据规则:
    • 操作 1 happens-before 操作 2(程序次序规则);
    • 操作 2 happens-before 操作 3(volatile 变量规则);
    • 操作 3 happens-before 操作 4(程序次序规则);
    • 因此操作 1 happens-before 操作 4(传递性),线程 B 读取 a 时能看到线程 A 设置的 a=1
3. volatile 无法保证原子性的原因及解决办法
(1)无法保证原子性的核心原因
  • 原子性定义:一个操作(或多个操作)要么全部执行且执行过程不被中断,要么全部不执行。
  • volatile 缺陷:仅保证可见性和禁止重排,但不保证复合操作的原子性
  • 典型示例volatile int count = 0; 执行 count++count++ 实际分 3 步:① 读取 count 值 ② 加 1 ③ 写回新值。即使 count 加了 volatile,多线程下仍会出现 “指令交错”:

    plaintext

    线程1:读count=0 → 加1=1(未写回)
    线程2:读count=0 → 加1=1(未写回)
    线程1:写回count=1
    线程2:写回count=1
    最终count=1(预期应为2)
    
(2)解决办法

针对 volatile 原子性不足的问题,有 3 种常用解决方案:

方案 1:使用 synchronized 关键字

通过同步锁将复合操作变为原子操作,适合低并发场景:

volatile int count = 0;

public void increment() {
    synchronized (this) { // 加锁保证count++原子性
        count++;
    }
}
方案 2:使用 JUC 原子类(推荐)

Java 并发包(java.util.concurrent.atomic)提供了原子操作类(如 AtomicIntegerAtomicLong),底层基于 CAS(Compare-And-Swap)实现无锁原子操作,性能优于 synchronized

AtomicInteger count = new AtomicInteger(0); // 无需加volatile

public void increment() {
    count.incrementAndGet(); // 原子自增,底层CAS操作
}

解释:CAS 包含 3 个参数(内存地址、预期值、新值),仅当内存中的值等于预期值时,才将其更新为新值,全程无锁,避免线程阻塞。

方案 3:使用 Lock 锁

通过 ReentrantLock 显式锁实现原子操作,灵活性更高(支持公平锁、可中断等):

volatile int count = 0;
Lock lock = new ReentrantLock();

public void increment() {
    lock.lock(); // 加锁
    try {
        count++;
    } finally {
        lock.unlock(); // 必须在finally中释放锁
    }
}

二、总结

  1. volatile 的核心能力是内存可见性(通过读写屏障同步主内存与工作内存)和禁止指令重排(通过内存屏障限制重排范围),底层依赖 CPU 内存屏障指令实现;
  2. volatilehappens-before 原则的重要体现,其写操作先行发生于后续读操作,且支持规则传递性;
  3. volatile 无法保证复合操作的原子性,解决办法优先选择 Atomic 原子类(无锁高效),其次是 synchronizedLock 锁。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐