杰瑞科技汇

Java多线程Thread如何高效创建与管理?

目录

  1. 为什么需要多线程?
  2. Java 多线程核心概念
    • 进程 vs. 线程
    • 并行 vs. 并发
    • 上下文切换
  3. 创建和启动线程
    • 继承 Thread
    • 实现 Runnable 接口
    • 实现 Callable 接口 (返回结果,可抛出异常)
    • 三种方法的对比与选择
  4. Thread 类的核心方法
    • 线程的生命周期
    • 控制线程的方法 (start(), run(), sleep(), join(), yield(), interrupt())
  5. 线程同步
    • 为什么需要同步?(可见性、原子性、有序性问题)
    • synchronized 关键字
    • volatile 关键字
    • Lock 接口 (如 ReentrantLock)
  6. 线程间通信
    • wait(), notify(), notifyAll()
  7. 线程池 (ExecutorService)
    • 为什么需要线程池?
    • ExecutorService 的使用
    • Executors 工厂类
  8. 现代异步编程:CompletableFuture

为什么需要多线程?

在单线程程序中,代码是顺序执行的,如果一个任务非常耗时(如网络请求、文件读写、复杂计算),整个程序都会被阻塞,直到这个任务完成,无法响应用户的其他操作。

Java多线程Thread如何高效创建与管理?-图1
(图片来源网络,侵删)

多线程的主要目的就是并发执行,让程序能够同时处理多个任务,从而:

  • 提高 CPU 利用率:当一个线程 I/O 阻塞时,CPU 可以切换到其他就绪的线程去执行,而不是空闲等待。
  • 提升程序响应速度:在 GUI 应用中,可以将耗时操作放在后台线程,避免界面卡死。
  • 简化程序模型:对于一些可以分解为多个独立子任务的问题,用多线程来处理逻辑更清晰。

Java 多线程核心概念

进程 vs. 线程

  • 进程:是操作系统进行资源分配和调度的基本单位,一个进程可以包含一个或多个线程,进程之间内存空间是独立的。
  • 线程:是进程中的一个执行流,是 CPU 调度的基本单位,同一进程内的线程共享该进程的内存空间(堆内存和方法区),但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。

简单比喻:一个进程就像一个工厂(进程),工厂里有多个车间(线程),它们共享原材料(堆内存),但每个车间有自己的工作台(栈内存)。

并行 vs. 并发

  • 并发:指两个或多个任务在同一时间间隔内宏观上同时运行,但在微观上是交替执行的,在单核 CPU 上,通过快速切换线程,让用户感觉它们在同时运行。
  • 并行:指两个或多个任务在同一时间真正地同时运行,在多核 CPU 上,不同的任务可以分配到不同的核心上同时执行。

上下文切换

CPU 只能同时执行一个线程,当操作系统需要从当前线程切换到另一个线程时,需要保存当前线程的执行状态(如程序计数器、寄存器等),并加载另一个线程的状态,这个过程称为上下文切换,频繁的上下文切换会带来性能开销。


创建和启动线程

Java 中创建线程主要有三种方式。

Java多线程Thread如何高效创建与管理?-图2
(图片来源网络,侵删)

继承 Thread

这是最简单的方式,但缺点是 Java 不支持多重继承,如果一个类已经继承了另一个类,就无法再继承 Thread

// 1. 定义一个类继承 Thread 类
class MyThread extends Thread {
    // 2. 重写 run() 方法,这是线程要执行的任务
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Thread " + Thread.currentThread().getName() + " is running, i = " + i);
            try {
                Thread.sleep(500); // 休眠 500 毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        // 3. 创建线程对象
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        // 4. 启动线程 (调用 start() 方法,而不是 run())
        thread1.start();
        thread2.start();
        System.out.println("Main thread is running.");
    }
}

关键点

  • 必须调用 start() 方法来启动线程。start() 会告诉 JVM 创建一个新的线程,并在这个新线程中调用 run() 方法。
  • 如果直接调用 run(),它就只是一个普通的方法调用,不会创建新线程,会在当前线程中顺序执行。

实现 Runnable 接口

这是更常用、更灵活的方式,它将任务(run 方法)和线程(Thread 类)分离开。

// 1. 定义一个类实现 Runnable 接口
class MyRunnable implements Runnable {
    // 2. 实现 run() 方法
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Runnable Thread " + Thread.currentThread().getName() + " is running, i = " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class RunnableDemo {
    public static void main(String[] args) {
        // 3. 创建 Runnable 实例
        MyRunnable runnable = new MyRunnable();
        // 4. 创建 Thread 对象,并将 Runnable 实例作为参数传入
        Thread thread1 = new Thread(runnable, "Thread-A");
        Thread thread2 = new Thread(runnable, "Thread-B");
        // 5. 启动线程
        thread1.start();
        thread2.start();
        System.out.println("Main thread is running.");
    }
}

优点

Java多线程Thread如何高效创建与管理?-图3
(图片来源网络,侵删)
  • 避免了单继承的限制。
  • 将任务和线程解耦,多个线程可以共享同一个 Runnable 实例。

实现 Callable 接口

Callable 是 Java 1.5 引入的,功能类似于 Runnable,但它更强大:

  1. call() 方法可以返回一个结果。
  2. call() 方法可以抛出异常。

Callable 通常与 FutureFutureTask 配合使用,用于获取异步执行的结果。

import java.util.concurrent.*;
// 1. 定义一个类实现 Callable 接口
// 泛型指定 call() 方法的返回类型
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 5; i++) {
            sum += i;
            System.out.println("Callable Thread " + Thread.currentThread().getName() + " is running, sum = " + sum);
            Thread.sleep(500);
        }
        return sum; // 返回计算结果
    }
}
public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 2. 创建 Callable 实例
        MyCallable callable = new MyCallable();
        // 3. 创建 FutureTask 对象,将 Callable 实例传入
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        // 4. 创建 Thread 对象,将 FutureTask 对象作为参数传入
        Thread thread = new Thread(futureTask, "Callable-Thread");
        thread.start();
        // 5. 主线程可以继续做其他事情...
        System.out.println("Main thread is doing other work...");
        // 6. 获取线程执行的结果 (这会阻塞主线程,直到子线程执行完毕)
        Integer result = futureTask.get();
        System.out.println("The result from Callable is: " + result);
    }
}

三种方法的对比与选择

特性 继承 Thread 实现 Runnable 接口 实现 Callable 接口
优点 编写简单,this 即代表当前线程 解耦任务与线程,支持共享资源 可以返回结果,可以抛出异常
缺点 继承受限,任务与线程耦合 获取结果不方便 (需共享变量) 代码稍复杂,需配合 Future 使用
适用场景 简单的、不需要共享任务实例的线程任务 最常用,推荐首选 需要获取异步执行结果的场景
返回值 有 (通过 Future.get() 获取)
异常处理 run 方法内部用 try-catch run 方法内部用 try-catch 可以直接在 call 方法中 throws

选择建议

  • 优先选择 Runnable,因为它更灵活、更符合面向对象的设计原则。
  • 如果需要从线程中获取返回值,使用 Callable
  • 除非有特殊且简单的需求,否则尽量避免直接继承 Thread

Thread 类的核心方法

线程的生命周期

一个线程从创建到销毁,会经历以下状态(Java 5 引入 java.lang.Thread.State 枚举):

  1. NEW (新建):线程被创建,但尚未调用 start() 方法。
  2. RUNNABLE (运行):线程调用了 start() 方法,此时它可能正在运行,也可能在等待 CPU 时间片,在操作系统中,它处于“就绪”或“运行”状态。
  3. BLOCKED (阻塞):线程因为等待监视器锁(即进入 synchronized 代码块或方法)而阻塞。
  4. WAITING (等待):线程进入等待状态,直到其他线程调用 notify()notifyAll() 才能唤醒,调用 Object.wait()Thread.join()
  5. TIMED_WAITING (超时等待):和 WAITING 类似,但它可以在指定时间后自动唤醒,调用 Thread.sleep(long millis)Object.wait(long timeout)Thread.join(long millis)
  6. TERMINATED (终止):线程执行完毕或因异常退出。

控制线程的方法

  • void start(): 启动线程,JVM 会调用该线程的 run() 方法。
  • void run(): 线程的执行体,继承 Thread 时需要重写此方法。
  • static void sleep(long millis): 让当前线程休眠指定的毫秒数,进入 TIMED_WAITING 状态,会抛出 InterruptedException
  • void join(): 等待该线程终止。thread1.join() 会让当前线程(如主线程)等待 thread1 执行完毕。
  • static void yield(): 提示当前线程让出 CPU,给其他线程一个执行的机会,这只是一个“建议”,操作系统不一定会采纳。
  • void interrupt(): 中断线程,它不会强制终止线程,而是设置一个中断状态,线程可以通过 isInterrupted()Thread.interrupted() 检查状态,并决定如何响应(如优雅地结束循环)。
  • boolean isAlive(): 测试线程是否处于活动状态(RUNNABLE, BLOCKED, WAITING, TIMED_WAITING)。

线程同步

当多个线程同时访问和修改共享数据时,可能会导致数据不一致、程序错误等问题,这就是线程安全问题。

为什么需要同步?

  1. 原子性:一个或多个操作要么全部执行成功,要么全部不执行。i++ 不是原子操作,它包含“读取-修改-写入”三个步骤。
  2. 可见性:一个线程对共享变量的修改,对其他线程是可见的,由于 CPU 缓存的存在,一个线程修改的值可能不会立即写回主内存,导致其他线程看不到最新的值。
  3. 有序性:程序的执行顺序应该按照代码的先后顺序执行,编译器和处理器可能会为了优化而对指令进行重排序。

synchronized 关键字

synchronized 是 Java 内置的锁机制,可以保证原子性、可见性和有序性。

它有两种用法:

  1. 修饰实例方法:锁是当前对象实例(this),同一时间只有一个线程能进入该对象的这个同步方法。
    public synchronized void increment() {
        count++;
    }
  2. 修饰静态方法:锁是当前类的 Class 对象,同一时间只有一个线程能进入该类的这个同步静态方法。
    public static synchronized void decrement() {
        count--;
    }
  3. 修饰代码块:可以指定锁对象,灵活性更高。
    public void increment() {
        synchronized (this) { // 锁是 this
            count++;
        }
    }

示例:不安全的计数器

class UnsafeCounter {
    private int count = 0;
    public void increment() {
        count++; // 非原子操作
    }
    public int getCount() {
        return count;
    }
}
public class UnsafeDemo {
    public static void main(String[] args) throws InterruptedException {
        UnsafeCounter counter = new UnsafeCounter();
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 预期结果是 2000,但实际结果通常小于 2000
        System.out.println("Final count: " + counter.getCount());
    }
}

示例:使用 synchronized 修复

class SafeCounter {
    private int count = 0;
    // 使用 synchronized 保证 increment 的原子性
    public synchronized void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}
// ... main 方法同上,但使用 SafeCounter ...
// 最终输出会是 2000

volatile 关键字

volatile 关键字比 synchronized 更轻量级,它主要用于保证可见性禁止指令重排序

  • 保证可见性:当一个线程修改了 volatile 变量,新值会立刻同步到主内存,并且其他线程读取时会从主内存读取,保证了线程间的可见性。
  • 不保证原子性volatile 不能保证 i++ 这样的复合操作是原子的。
  • 禁止指令重排序:可以防止 JVM 和 CPU 为了优化而对代码进行重排序。

适用场景:一个线程写,多个线程读的共享变量,状态标志位。

class VolatileExample {
    // volatile 保证了 flag 的可见性
    private volatile boolean flag = false;
    public void writer() {
        flag = true; // 对 flag 的修改对其他线程立即可见
    }
    public void reader() {
        while (!flag) {
            // 等待 flag 变为 true
        }
        System.out.println("Flag is now true!");
    }
}

Lock 接口 (java.util.concurrent.locks)

synchronized 是隐式锁,而 Lock 提供了更灵活的显式锁,最常用的是 ReentrantLock

ReentrantLock vs synchronized:

  • 功能ReentrantLock 提供了更强大的功能,如可中断的锁获取、可轮询的锁获取、公平锁等。
  • 使用方式synchronized 自动释放锁,而 Lock 必须在 finally 块中手动调用 unlock() 来释放锁,否则会导致死锁。
  • 性能:在竞争不激烈时,两者性能相当,在竞争激烈时,ReentrantLock 的性能可能更好。
import java.util.concurrent.locks.ReentrantLock;
class LockCounter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();
    public void increment() {
        lock.lock(); // 加锁
        try {
            count++; // 临界区代码
        } finally {
            lock.unlock(); // 在 finally 中确保锁一定会被释放
        }
    }
    public int getCount() {
        return count;
    }
}

线程间通信

wait(), notify(), notifyAll()Object 类的方法,它们用于实现线程间的等待/通知机制,通常配合 synchronized 使用。

  • void wait(): 让当前线程等待,直到其他线程调用此对象的 notify()notifyAll() 方法,调用 wait() 的线程会释放锁。
  • void notify(): 唤醒在此对象监视器上等待的单个线程(选择哪个线程是随机的)。
  • void notifyAll(): 唤醒在此对象监视器上等待的所有线程。

经典示例:生产者-消费者模型

class SharedResource {
    private int item = 0;
    private boolean isProduced = false;
    // 生产者
    public synchronized void produce() throws InterruptedException {
        // 如果已经有产品,就等待消费者消费
        while (isProduced) {
            wait();
        }
        item++;
        System.out.println("Produced: " + item);
        isProduced = true;
        notifyAll(); // 唤醒可能在等待的消费者
    }
    // 消费者
    public synchronized void consume() throws InterruptedException {
        // 如果没有产品,就等待生产者生产
        while (!isProduced) {
            wait();
        }
        System.out.println("Consumed: " + item);
        isProduced = false;
        notifyAll(); // 唤醒可能在等待的生产者
    }
}
class Producer implements Runnable {
    private SharedResource resource;
    public Producer(SharedResource resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                resource.produce();
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Consumer implements Runnable {
    private SharedResource resource;
    public Consumer(SharedResource resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                resource.consume();
                Thread.sleep(150);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ProducerConsumerDemo {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();
        new Thread(new Producer(resource)).start();
        new Thread(new Consumer(resource)).start();
    }
}

注意wait() 必须在 synchronized 代码块或方法中调用,否则会抛出 IllegalMonitorStateException,通常使用 while 循环来检查条件,而不是 if,以防虚假唤醒。


线程池 (ExecutorService)

频繁地创建和销毁线程是非常消耗资源的,线程池就是为了解决这个问题而设计的,它预先创建一组线程,当有任务需要执行时,就从线程池中取出一个线程来执行,任务执行完毕后,线程不会销毁,而是返回线程池中,等待下一个任务。

为什么需要线程池?

  • 降低资源消耗:减少创建和销毁线程的开销。
  • 提高响应速度:任务到达时,无需等待创建线程即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,线程池可以统一地分配、调优和管理。

ExecutorService 的使用

ExecutorService 是线程池的核心接口。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 1. 创建一个固定大小的线程池 (2 个线程)
        ExecutorService executor = Executors.newFixedThreadPool(2);
        // 2. 提交任务到线程池
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is running by " + Thread.currentThread().getName());
                try {
                    TimeUnit.SECONDS.sleep(2); // 模拟耗时任务
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Task " + taskId + " is finished.");
            });
        }
        // 3. 关闭线程池
        // shutdown() 会停止接受新任务,但会执行完队列中已有的任务
        executor.shutdown();
        // 检查线程池是否已经关闭
        try {
            // 等待所有任务完成,最多等待 1 小时
            if (!executor.awaitTermination(1, TimeUnit.HOURS)) {
                System.out.println("Some tasks were not terminated.");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Main thread finished.");
    }
}

Executors 工厂类

Executors 提供了多种创建线程池的便捷方法:

  • newFixedThreadPool(int n): 创建一个固定大小的线程池。
  • newCachedThreadPool(): 创建一个可缓存的线程池,如果线程池大小超过处理任务所需的线程,就会回收空闲线程;如果需求增加,则会创建新线程。
  • newSingleThreadExecutor(): 创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,保证任务按顺序执行。
  • newScheduledThreadPool(int corePoolSize): 创建一个可以执行延迟任务或周期性任务的线程池。

注意:阿里巴巴 Java 开发手册中明确指出,不允许使用 Executors 创建线程池,而是要通过 ThreadPoolExecutor 的方式创建,以避免资源耗尽的风险。Executors 创建的线程池在极端情况下(如大量任务)可能会导致 OOM。


现代异步编程:CompletableFuture

CompletableFuture 是 Java 8 引入的一个强大的异步编程工具,它是对 Future 的增强。Future 提供了一种异步计算的方式,但它使用起来不够方便,无法对计算结果进行链式处理和组合。

CompletableFuture 的特点:

  • 非阻塞:可以链式调用方法,对结果进行处理。
  • 函数式编程:支持 Function, Consumer, Runnable 等函数式接口。
  • 组合能力:可以组合多个 CompletableFuture,实现复杂的异步流程。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1. 创建一个异步任务
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello";
        });
        // 2. 链式处理结果
        CompletableFuture<String> resultFuture = future
                .thenApply(s -> s + " World") // 上一步成功后,对结果进行处理
                .thenApply(s -> s + "!");
        // 3. 阻塞获取最终结果
        System.out.println("Final result: " + resultFuture.get()); // 会阻塞直到结果完成
        // 4. 组合多个 CompletableFuture
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);
        // 当 future1 和 future2 都完成后,将它们的结果相加
        CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (a, b) -> a + b);
        System.out.println("Combined result: " + combinedFuture.get());
    }
}

概念/技术 核心作用 关键点/用法
Thread 线程的底层实现 start(), run(), sleep(), join()
Runnable 定义任务,与线程解耦 实现 run() 方法,传递给 Thread
Callable/Future 异步任务,可返回结果 实现 call() 方法,用 Future.get() 获取结果
同步 解决线程安全问题 synchronized (隐式锁), ReentrantLock (显式锁), volatile (可见性)
线程通信 线程间协作 wait(), notify(), notifyAll() (配合 synchronized)
线程池 复用线程,提高性能,管理线程 ExecutorService, Executors (或 ThreadPoolExecutor)
异步编程 现代、非阻塞的异步处理方式 CompletableFuture (链式调用、组合)

掌握 Java 多线程是一个循序渐进的过程,建议从 Runnablesynchronized 开始,逐步学习线程池和 CompletableFuture,并深刻理解线程同步和通信的原理,这样才能编写出高效、健壮的并发程序。

分享:
扫描分享到社交APP
上一篇
下一篇