多线程是 Java 编程中处理并发任务的核心机制,它允许程序同时执行多个任务,显著提升资源利用率和程序响应速度。本文将从线程的基本概念出发,系统讲解 Java 多线程的实现方式、核心原理及实践要点。

一、线程与进程:并发的基础概念

  • 进程:操作系统分配资源的基本单位,每个进程拥有独立的内存空间、文件描述符等资源,进程间相互隔离。例如,打开的浏览器、IDE 都是独立进程。
  • 线程:进程内的执行单元,共享进程的资源(内存、文件等),是 CPU 调度的基本单位。一个进程可以包含多个线程,线程间切换的开销远小于进程。

为什么需要多线程?

  • 提高 CPU 利用率:当一个线程因 IO 操作(如读写文件、网络请求)阻塞时,其他线程可继续执行;
  • 提升程序响应速度:例如 GUI 程序中,后台线程处理数据,主线程保持界面响应;
  • 简化复杂任务:将大任务拆分为多个并行子任务,缩短整体执行时间。

二、Java 中创建线程的三种方式

1. 继承 Thread 类

Thread 类是 Java 线程的基础类,通过继承它并重写run()方法定义线程任务:

class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的任务
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程执行:" + i);
        }
    }
}

// 使用方式
public class Test {
    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start(); // 启动线程(底层调用run())
        // 主线程任务
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行:" + i);
        }
    }
}

注意:必须调用start()而非直接调用run()start()会触发 JVM 创建新线程并执行run(),直接调用run()只是普通方法调用,不会启动新线程。

2. 实现 Runnable 接口

Runnable 是函数式接口(仅含run()方法),通过实现它定义任务,再传入 Thread 类:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程任务
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程执行:" + i);
        }
    }
}

// 使用方式
public class Test {
    public static void main(String[] args) {
        Runnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

优势:避免单继承限制(一个类可同时实现 Runnable 和其他接口),更适合多线程共享任务逻辑的场景。

3. 实现 Callable 接口(带返回值)

Callable 接口与 Runnable 类似,但call()方法可返回结果且能抛出异常,需配合 FutureTask 使用:

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

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 10; i++) {
            sum += i;
        }
        return sum; // 返回计算结果
    }
}

// 使用方式
public class Test {
    public static void main(String[] args) throws Exception {
        Callable<Integer> callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        
        // 获取子线程返回值(会阻塞主线程直到结果返回)
        int result = futureTask.get();
        System.out.println("计算结果:" + result); // 输出55
    }
}

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

Java 线程有 6 种状态(定义在Thread.State枚举中),状态转换是多线程控制的核心:

  1. 新建(New):线程对象创建后,未调用start()前的状态。
  2. 就绪(Runnable):调用start()后,线程进入就绪队列等待 CPU 调度(此时可能正在运行,也可能等待)。
  3. 运行(Running):CPU 调度线程执行run()方法的状态。
  4. 阻塞(Blocked):线程因竞争同步锁失败而暂停(如进入synchronized代码块未获取锁)。
  5. 等待(Waiting):线程调用wait()join()等方法后,需其他线程唤醒才能继续(无超时时间)。
  6. 超时等待(Timed Waiting):线程调用sleep(long)wait(long)等方法,等待指定时间后自动唤醒。
  7. 终止(Terminated)run()方法执行完毕或线程被中断,线程生命周期结束。

核心转换逻辑:新建 → 就绪(start())→ 运行(CPU 调度)→ 终止(任务完成)运行 → 阻塞(竞争锁失败)→ 就绪(获取锁)运行 → 等待 / 超时等待(调用相关方法)→ 就绪(被唤醒 / 超时)

四、线程控制的核心方法

方法作用注意事项
start()启动线程,进入就绪状态不可重复调用,否则抛IllegalThreadStateException
run()线程执行的任务逻辑无需手动调用,由start()触发
sleep(long ms)让线程休眠指定毫秒,释放 CPU 资源不会释放同步锁,休眠期间可被interrupt()中断
wait()/notify()线程间通信,释放同步锁等待 / 唤醒必须在synchronized代码块中调用
join()让当前线程等待目标线程执行完毕可指定超时时间,避免无限等待
interrupt()中断线程(设置中断标志,非强制终止)需线程主动检测isInterrupted()响应中断
yield()礼让线程,让当前线程重新进入就绪状态仅为建议,CPU 可能忽略此请求

五、线程安全与同步机制

当多个线程共享资源时,可能出现数据不一致(如并发修改同一变量),需通过同步机制保证线程安全:

1. synchronized 关键字

  • 作用:保证同一时刻只有一个线程执行同步代码块 / 方法,实现互斥访问。
  • 使用方式
    // 同步方法
    public synchronized void method() { ... }
    
    // 同步代码块(锁对象可为任意对象,推荐用当前对象this或类对象)
    public void method() {
        synchronized (this) { ... }
    }

  • 原理:基于对象内置锁(monitor)实现,进入同步块获取锁,退出释放锁。
  • 2. Lock 接口(JDK 1.5+)

    synchronized更灵活,支持尝试获取锁、超时释放等功能,常用实现类ReentrantLock

  • import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Test {
        private Lock lock = new ReentrantLock();
        
        public void method() {
            lock.lock(); // 获取锁
            try {
                // 临界区代码
            } finally {
                lock.unlock(); // 必须在finally中释放锁,避免死锁
            }
        }
    }

3. 线程安全的容器

JDK 提供了线程安全的集合类,如VectorHashTable,或通过Collections.synchronizedXXX()包装普通集合。更高效的并发容器(如ConcurrentHashMapCopyOnWriteArrayList)位于java.util.concurrent包,适合高并发场景。

六、线程池:高效管理线程资源

频繁创建 / 销毁线程会消耗大量资源,线程池通过复用线程提高性能,是企业开发的首选方案。

核心接口与类

  • Executor:线程池顶层接口;
  • ExecutorService:扩展 Executor,提供线程池生命周期管理;
  • ThreadPoolExecutor:线程池核心实现类;
  • Executors:线程池工具类,提供快捷创建方法(如newFixedThreadPoolnewCachedThreadPool)。

基本使用

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
    public static void main(String[] args) {
        // 创建固定大小的线程池(3个线程)
        ExecutorService pool = Executors.newFixedThreadPool(3);
        
        // 提交任务
        for (int i = 0; i < 5; i++) {
            pool.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "执行任务");
            });
        }
        
        // 关闭线程池(不再接受新任务,等待现有任务完成)
        pool.shutdown();
    }
}

七、多线程常见问题与解决方案

  1. 死锁:两个线程互相持有对方需要的锁,导致无限等待。解决:避免嵌套锁、按固定顺序获取锁、设置锁超时时间。

  2. 线程泄露:线程未正常终止,长期占用资源。解决:使用线程池管理线程,避免手动创建线程;确保线程能响应中断。

  3. 竞态条件:多线程并发修改共享变量导致数据错误。解决:使用synchronizedLock同步;使用原子类(如AtomicInteger)。

结语

多线程是 Java 并发编程的基础,掌握它需要理解线程生命周期、同步机制及资源管理。实际开发中,应优先使用线程池而非手动创建线程,结合java.util.concurrent包提供的工具类(如CountDownLatchSemaphore)处理复杂并发场景。理解多线程的核心是平衡效率与安全,在保证数据一致性的前提下最大化利用系统资源。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐