线程是Java并发编程的核心基石,也是面试与企业开发中的高频重点。从基础的线程创建与启动,到复杂的线程同步、线程池优化,再到JUC并发工具类的实战,掌握线程知识不仅能解决日常开发中的并发问题,更能深入理解JVM底层运行机制。本文将系统性梳理Java线程体系,结合实例拆解核心难点,助力开发者构建扎实的并发编程能力。

一、核心概念:线程与进程的本质区别

在深入线程之前,需先明确线程与进程的关系,这是理解并发编程的前提。

1.1 进程与线程的定义

  • 进程:操作系统资源分配的最小单位,是一个独立的程序运行实例。每个进程拥有独立的内存空间(堆、方法区)、文件描述符等资源,进程间切换开销较大。例如,打开一个浏览器窗口就是一个进程。

  • 线程:进程内的执行单元,是CPU调度的最小单位。多个线程共享所属进程的资源(堆、方法区),仅拥有独立的程序计数器、虚拟机栈、本地方法栈,线程切换开销远小于进程。例如,浏览器中同时加载页面、播放视频的操作,对应多个线程。

1.2 核心区别与关联

对比维度 进程 线程
资源分配 独立资源(内存、文件句柄) 共享进程资源,仅私有少量数据
调度单位 操作系统层面调度,开销大 CPU层面调度,开销小
并发能力 进程间并发(基于IPC通信) 线程间并发(基于共享内存)
稳定性 进程崩溃不影响其他进程 线程崩溃可能导致整个进程终止

1.3 线程的核心意义

Java引入线程的核心价值的是提升程序执行效率,充分利用CPU资源:

  • 并行执行:多线程可在多核CPU上同时执行,避免单线程独占CPU导致的资源浪费。

  • 异步解耦:将耗时操作(如IO、网络请求)放入独立线程,避免阻塞主线程,提升程序响应速度。

  • 任务拆分:复杂任务可拆分为多个子任务并行执行,缩短整体耗时(如批量数据处理)。

二、Java线程基础:创建与启动

Java提供三种线程创建方式,各有适用场景,需掌握其实现原理与优缺点。

2.1 方式1:继承Thread类

Thread类是Java线程的核心类,继承后重写run()方法定义线程任务,通过start()方法启动线程。



// 继承Thread类,重写run()方法
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行任务
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 执行:" + i);
            try {
                Thread.sleep(500); // 模拟耗时操作,释放CPU资源
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试启动
public class ThreadTest {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        thread1.setName("线程1");
        thread2.setName("线程2");
        // 启动线程(底层调用start0()native方法,由JVM创建操作系统线程)
        thread1.start(); 
        thread2.start();
        // 注意:直接调用run()方法不会启动新线程,仅普通方法调用
        // thread1.run(); 
    }
}

优缺点

  • 优点:实现简单,可直接通过this获取当前线程。

  • 缺点:Java单继承特性,继承Thread后无法再继承其他类,灵活性差;任务与线程耦合,可复用性低。

2.2 方式2:实现Runnable接口

Runnable接口仅定义run()方法,通过将任务与线程分离,解决单继承限制,是更推荐的基础方式。



// 实现Runnable接口,定义任务
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 执行:" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试启动
public class RunnableTest {
    public static void main(String[] args) {
        // 创建任务实例
        MyRunnable task = new MyRunnable();
        // 传入任务,创建线程并启动
        Thread thread1 = new Thread(task, "线程1");
        Thread thread2 = new Thread(task, "线程2");
        thread1.start();
        thread2.start();
    }
}

优缺点

  • 优点:规避单继承限制,任务与线程解耦,可复用任务实例(多个线程共享一个任务)。

  • 缺点:run()方法无返回值,无法抛出受检异常,无法获取任务执行结果。

2.3 方式3:实现Callable接口+FutureTask

Callable接口是JDK 1.5引入的增强方式,支持返回任务结果、抛出受检异常,配合FutureTask可实现异步结果获取。



import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 实现Callable接口,泛型指定返回值类型
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 10; i++) {
            sum += i;
            Thread.sleep(200);
        }
        return sum; // 返回任务结果
    }
}

// 测试启动
public class CallableTest {
    public static void main(String[] args) {
        // 创建Callable任务
        MyCallable callable = new MyCallable();
        // FutureTask包装任务,兼具Runnable和Future特性
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        // 启动线程
        new Thread(futureTask, "计算线程").start();

        // 主线程获取结果(会阻塞,直到任务执行完成)
        try {
            // isDone()判断任务是否完成,避免阻塞(可选)
            while (!futureTask.isDone()) {
                System.out.println("任务执行中...");
                Thread.sleep(300);
            }
            // get()获取返回结果,若任务抛出异常,get()会封装为ExecutionException
            Integer result = futureTask.get();
            System.out.println("任务执行结果:" + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

核心特性

  • FutureTask实现了Runnable和Future接口,可作为任务传入Thread启动,同时通过Future方法控制任务。

  • 关键方法:get()(获取结果,阻塞)、isDone()(判断任务是否完成)、cancel()(取消任务)、isCancelled()(判断任务是否被取消)。

  • 适用场景:需要获取异步任务结果的场景(如并行计算、接口调用超时控制)。

三、线程生命周期与状态转换

Java线程有6种核心状态,由Thread.State枚举定义,理解状态转换是解决线程并发问题的关键。

3.1 6种核心状态

  • NEW(新建状态):线程对象已创建,但未调用start()方法,此时未与操作系统线程关联。

  • RUNNABLE(可运行状态):调用start()后,线程进入此状态,包含“就绪”和“运行中”两个子状态。就绪状态等待CPU调度,调度后进入运行中状态执行run()方法。

  • BLOCKED(阻塞状态):线程等待同步锁(synchronized)时进入此状态,获取锁后返回RUNNABLE。

  • WAITING(无限等待状态):通过wait()、join()等方法进入,无超时时间,需其他线程唤醒(notify()/notifyAll())才能返回RUNNABLE。

  • TIMED_WAITING(计时等待状态):通过sleep(long)、wait(long)、join(long)等方法进入,超时后自动唤醒,返回RUNNABLE。

  • TERMINATED(终止状态):线程任务执行完成(run()方法结束)或异常终止,状态不可逆转。

3.2 状态转换流程图

渲染错误: Mermaid 渲染失败: Parse error on line 2: ... A[NEW] -->|start()| B[RUNNABLE] -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

3.3 关键方法对状态的影响

方法 调用线程状态 状态变化结果 注意事项
start() NEW 转为RUNNABLE 仅能调用一次,多次调用抛IllegalThreadStateException
sleep(long) RUNNABLE 转为TIMED_WAITING 不释放同步锁,休眠期间可被interrupt()中断
wait() RUNNABLE(持有锁) 释放锁,转为WAITING 必须在synchronized代码块中调用,唤醒后需重新竞争锁
notify() 任意状态(持有锁) 唤醒一个WAITING线程,使其进入RUNNABLE 需在synchronized代码块中调用,仅唤醒随机一个线程
join() 调用其他线程的线程 调用线程转为WAITING 等待目标线程执行完成,底层依赖wait()实现
interrupt() WAITING/TIMED_WAITING 抛出InterruptedException,线程终止 无法中断RUNNABLE状态的线程,需手动检测中断标志

四、线程同步与线程安全

多线程共享资源时,会出现竞态条件(Race Condition)导致线程安全问题,需通过同步机制保证原子性、可见性、有序性。

4.1 线程安全三大核心特性

  • 原子性:操作不可分割,要么全部执行,要么全部不执行(如i++并非原子操作,需拆分为读取、加1、写入三步)。

  • 可见性:一个线程修改共享变量后,其他线程能立即感知到该变化(避免CPU缓存导致的变量不一致)。

  • 有序性:线程执行顺序按代码逻辑执行,避免JVM指令重排导致的执行混乱。

4.2 核心同步机制实现

4.2.1 synchronized关键字(内置锁)

synchronized是Java原生同步机制,基于对象监视器(Monitor)实现,自动获取/释放锁,支持方法同步和代码块同步。



// 1. 实例方法同步(锁为当前对象this)
class SyncService {
    public synchronized void method1() {
        // 同步代码
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 2. 静态方法同步(锁为当前类的Class对象)
    public static synchronized void method2() {
        // 同步代码
    }

    // 3. 代码块同步(锁为指定对象,灵活性最高)
    public void method3() {
        Object lock = new Object();
        synchronized (lock) {
            // 同步代码
        }
    }
}

锁升级机制(JDK 1.6优化):

synchronized锁并非一开始就是重量级锁,而是通过“偏向锁→轻量级锁→重量级锁”的升级路径优化性能:

  • 偏向锁:单线程场景下,锁偏向第一个获取锁的线程,减少锁竞争开销。

  • 轻量级锁:多线程交替获取锁时,通过CAS操作竞争锁,避免阻塞线程。

  • 重量级锁:多线程同时竞争锁时,升级为重量级锁,依赖操作系统互斥量实现,线程会阻塞。

4.2.2 Lock接口(显式锁)

JDK 1.5引入的java.util.concurrent.locks.Lock接口,提供比synchronized更灵活的同步控制,支持手动获取/释放锁、公平锁、可中断锁等特性。



import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class LockService {
    // ReentrantLock:可重入锁,默认非公平锁
    private final Lock lock = new ReentrantLock();

    public void doTask() {
        // 手动获取锁(可尝试获取、超时获取、可中断获取)
        lock.lock();
        try {
            // 同步代码
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " 执行任务");
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 必须在finally中释放锁,避免死锁
            lock.unlock();
        }
    }

    // 尝试获取锁(非阻塞)
    public void tryLockTask() {
        if (lock.tryLock()) {
            try {
                System.out.println(Thread.currentThread().getName() + " 获取锁成功");
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " 获取锁失败");
        }
    }
}

synchronized与Lock对比:

特性 synchronized Lock(ReentrantLock)
锁获取/释放 自动获取/释放,无需手动操作 手动获取(lock())/释放(unlock()),需在finally中释放
可重入性 支持可重入 支持可重入
公平锁 仅非公平锁 支持公平锁(构造函数传true)和非公平锁
锁竞争策略 线程阻塞,效率较低 支持非阻塞获取、超时获取、可中断获取,灵活性更高
条件变量 通过wait()/notify()实现,功能简单 通过Condition接口实现,支持多条件等待

4.2.3 volatile关键字(轻量级同步)

volatile仅保证共享变量的可见性和有序性,不保证原子性,适用于单线程写、多线程读的场景。



class VolatileTest {
    // volatile修饰共享变量,保证可见性和有序性
    private volatile boolean flag = false;

    public void setFlag() {
        this.flag = true; // 写操作,立即刷新到主内存
    }

    public void doTask() {
        while (!flag) {
            // 读操作,每次从主内存获取最新值
        }
        System.out.println("任务执行完成");
    }
}

核心作用

  • 禁止指令重排:避免JVM对volatile变量相关指令重排,保证执行顺序。

  • 强制内存可见性:写操作时立即刷新到主内存,读操作时从主内存读取,避免CPU缓存不一致。

  • 注意:volatile无法解决原子性问题(如i++),需配合synchronized或原子类使用。

4.2.4 原子类(java.util.concurrent.atomic)

原子类基于CAS(Compare And Swap)操作实现原子性,无需锁机制,性能优于synchronized,适用于简单原子操作场景。



import java.util.concurrent.atomic.AtomicInteger;

class AtomicTest {
    // 原子整数,支持原子性的自增、自减等操作
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // 原子自增(i++)
    }

    public int getCount() {
        return count.get();
    }
}

// 测试多线程原子操作
public class AtomicDemo {
    public static void main(String[] args) throws InterruptedException {
        AtomicTest test = new AtomicTest();
        Runnable task = test::increment;

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("最终计数:" + test.getCount()); // 结果一定是2,无线程安全问题
    }
}

常用原子类

  • 基本类型原子类:AtomicInteger、AtomicLong、AtomicBoolean。

  • 引用类型原子类:AtomicReference、AtomicStampedReference(解决ABA问题)。

  • 数组类型原子类:AtomicIntegerArray、AtomicLongArray。

五、线程池:企业级并发核心工具

手动创建线程存在资源浪费、难以管理等问题,线程池通过池化技术复用线程,控制并发数量,是企业级开发的必备工具。

5.1 线程池核心原理

线程池维护一个线程队列和任务队列,核心流程:

  1. 提交任务时,若核心线程数未满,创建核心线程执行任务。

  2. 核心线程数满后,任务放入任务队列等待。

  3. 任务队列满后,若最大线程数未满,创建非核心线程执行任务。

  4. 最大线程数满后,执行拒绝策略处理多余任务。

  5. 非核心线程空闲时间超过keepAliveTime,自动销毁,释放资源。

5.2 核心参数(ThreadPoolExecutor)

ThreadPoolExecutor是线程池核心实现类,构造函数参数决定线程池行为:



public ThreadPoolExecutor(
    int corePoolSize, // 核心线程数(常驻线程,不会自动销毁)
    int maximumPoolSize, // 最大线程数(核心+非核心线程上限)
    long keepAliveTime, // 非核心线程空闲超时时间
    TimeUnit unit, // 超时时间单位
    BlockingQueue<Runnable> workQueue, // 任务队列(存储等待执行的任务)
    ThreadFactory threadFactory, // 线程工厂(自定义线程创建)
    RejectedExecutionHandler handler // 拒绝策略(任务满时的处理方式)
) { ... }

关键组件说明

  • 任务队列:常用LinkedBlockingQueue(无界队列)、ArrayBlockingQueue(有界队列)、SynchronousQueue(无缓冲队列)。

  • 拒绝策略:JDK提供4种默认策略(AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy),也可自定义。

5.3 常见线程池类型(Executors工具类)

Executors提供快捷创建线程池的方法,但部分方法存在隐患,需谨慎使用:

线程池类型 创建方法 核心配置 适用场景 隐患
FixedThreadPool Executors.newFixedThreadPool(n) 核心线程数=最大线程数,无界队列 固定并发量的任务(如服务器请求处理) 无界队列可能导致OOM
CachedThreadPool Executors.newCachedThreadPool() 核心线程数0,最大线程数Integer.MAX_VALUE,超时60s 短期、频繁的任务(如临时计算) 线程数无上限,可能导致CPU耗尽
ScheduledThreadPool Executors.newScheduledThreadPool(n) 核心线程数n,最大线程数Integer.MAX_VALUE 定时/周期性任务(如定时备份) 最大线程数无上限,需控制任务量
SingleThreadExecutor Executors.newSingleThreadExecutor() 核心线程数=最大线程数=1,无界队列 串行执行任务(如日志写入) 无界队列可能导致OOM

5.4 推荐实践:自定义线程池

生产环境中,禁止使用Executors创建线程池,需手动配置ThreadPoolExecutor,避免OOM和资源耗尽问题:



import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomThreadPool {
    // 自定义线程工厂(命名线程,便于排查问题)
    private static final ThreadFactory threadFactory = new ThreadFactory() {
        private final AtomicInteger count = new AtomicInteger(1);

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("custom-thread-" + count.getAndIncrement());
            return thread;
        }
    };

    // 自定义拒绝策略(打印日志+重试)
    private static final RejectedExecutionHandler rejectedHandler = (r, executor) -> {
        System.out.println("任务" + r + "被拒绝,线程池状态:" + executor);
        // 可选:重试提交(需控制次数,避免死循环)
        try {
            executor.getQueue().offer(r, 1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    };

    // 创建自定义线程池
    public static ThreadPoolExecutor createThreadPool() {
        int corePoolSize = Runtime.getRuntime().availableProcessors(); // 核心线程数=CPU核心数
        int maximumPoolSize = corePoolSize * 2; // 最大线程数=CPU核心数*2
        long keepAliveTime = 60;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable&gt; workQueue = new ArrayBlockingQueue<>(1024); // 有界队列,容量1024

        return new ThreadPoolExecutor(
            corePoolSize,
            maximumPoolSize,
            keepAliveTime,
            unit,
            workQueue,
            threadFactory,
            rejectedHandler
        );
    }

    // 测试
    public static void main(String[] args) {
        ThreadPoolExecutor executor = createThreadPool();
        for (int i = 0; i< 2000; i++) {
            int finalI = i;
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行任务:" + finalI);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executor.shutdown(); // 优雅关闭线程池(等待任务完成,不再接受新任务)
    }
}

六、JUC并发工具类实战

java.util.concurrent(JUC)包提供了丰富的并发工具类,简化复杂并发场景开发,以下为核心工具类实战。

6.1 CountDownLatch(倒计时器)

实现“主线程等待多个子线程执行完成后再继续”的场景,基于计数器实现,计数器为0时唤醒主线程。


import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        int threadNum = 5;
        CountDownLatch latch = new CountDownLatch(threadNum); // 计数器初始值=线程数

        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 执行任务");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 任务完成,计数器减1
                }
            }, "子线程" + (i+1)).start();
        }

        latch.await(); // 主线程阻塞,直到计数器为0
        System.out.println("所有子线程任务完成,主线程继续执行");
    }
}

6.2 CyclicBarrier(循环屏障)

实现“多个线程相互等待,达到屏障点后同时继续执行”的场景,支持循环复用。



import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {
    public static void main(String[] args) {
        int threadNum = 3;
        // 屏障点:3个线程到达后,执行屏障动作(可选)
        CyclicBarrier barrier = new CyclicBarrier(threadNum, () -> {
            System.out.println("所有线程到达屏障点,开始同时执行后续任务");
        });

        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 前往屏障点");
                    Thread.sleep((long) (Math.random() * 2000));
                    barrier.await(); // 到达屏障点,等待其他线程
                    // 屏障点后执行的任务
                    System.out.println(Thread.currentThread().getName() + " 执行后续任务");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "线程" + (i+1)).start();
        }
    }
}

6.3 Semaphore(信号量)

控制同时访问共享资源的线程数量,基于许可机制实现,获取许可后才能访问资源。



import java.util.concurrent.Semaphore;

public class SemaphoreTest {
    public static void main(String[] args) {
        int permitNum = 2; // 允许同时访问的线程数
        Semaphore semaphore = new Semaphore(permitNum);

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire(); // 获取许可,无许可则阻塞
                    System.out.println(Thread.currentThread().getName() + " 获取许可,访问资源");
                    Thread.sleep(1000); // 访问资源耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // 释放许可
                    System.out.println(Thread.currentThread().getName() + " 释放许可");
                }
            }, "线程" + (i+1)).start();
        }
    }
}

七、线程并发避坑指南与最佳实践

7.1 常见陷阱与解决方案

  • 陷阱1:死锁
    原因:多个线程相互持有对方所需的锁,陷入无限等待。
    解决方案:1. 按固定顺序获取锁;2. 限时获取锁(tryLock());3. 避免嵌套锁;4. 检测死锁(jstack命令)。

  • 陷阱2:线程泄漏
    原因:线程执行完成后未正确销毁(如线程池未关闭、任务阻塞无超时)。
    解决方案:1. 优雅关闭线程池(shutdown()/shutdownNow());2. 为阻塞操作设置超时时间;3. 避免线程永久阻塞。

  • 陷阱3:ABA问题原因:CAS操作中,变量从A改为B再改回A,导致CAS误判为未修改。
    解决方案:使用AtomicStampedReference(添加版本号),基于版本号判断变量是否被修改。

  • 陷阱4:过度同步
    原因:对无需同步的代码加锁,导致性能下降。
    解决方案:1. 最小化同步代码块范围;2. 优先使用volatile、原子类等轻量级同步机制;3. 读写分离(CopyOnWriteArrayList)。

7.2 企业级最佳实践

  1. 优先使用线程池管理线程,禁止手动创建线程(除特殊场景)。

  2. 自定义线程池时,使用有界队列+合理拒绝策略,避免OOM。

  3. 线程命名规范,便于日志排查(如“业务名称-线程编号”)。

  4. 避免使用ThreadLocal存储共享资源,防止内存泄漏(需手动remove())。

  5. 多线程场景下,优先使用JUC工具类,避免手动实现同步逻辑。

  6. 对敏感共享变量,使用volatile或原子类保证可见性和原子性。

  7. 定期监控线程状态(jstack、jconsole),及时发现死锁、阻塞问题。

八、总结与学习资源

Java线程是并发编程的基础,从线程创建、状态管理,到同步机制、线程池优化,再到JUC工具类实战,需层层递进掌握。核心要点在于理解线程安全三大特性,灵活运用同步机制和线程池,规避并发陷阱,在保证线程安全的前提下提升程序性能。

推荐学习资源:

  • 书籍:《Java并发编程实战》《Java并发编程的艺术》

  • 官方文档:Java Thread 官方文档

  • 工具:jstack(线程堆栈分析)、jconsole(线程监控)、Arthas(线上线程诊断)

后续可深入研究线程模型(M:N模型)、JVM线程调度机制、并发性能优化等高级主题,结合分布式场景下的并发问题(如分布式锁),构建完整的并发知识体系。

Logo

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

更多推荐