摘要

随着多核处理器普及,Java 并发编程已是后端与桌面应用中不可或缺的一部分。本文从概念、常见写法到常见问题与最佳实践,对 Java 多线程进行系统梳理,并补充了线程池、Callable/Future、并发工具类与实用建议。

目录

  1. 引言

  2. 线程是什么(Java 角度)

  3. 常见创建线程的方式

  4. 更现代的并发:线程池与异步任务

  5. 同步、可见性与线程安全

  6. 常见并发工具与用法速查

  7. 常见陷阱与调试技巧

  8. 实际开发中的最佳实践

  9. 总结与参考标签

1 引言

多核 CPU 把并行计算带到了桌面与服务器端。单靠顺序执行无法充分利用硬件并行能力,Java 提供了线程与丰富的并发库,使我们可以把工作拆成多个并发任务来提高吞吐与响应能力。但并发也带来复杂性:共享状态、竞态、死锁、内存可见性等问题必须谨慎处理。

2 线程是什么(Java 角度)

在 Java 中,线程是程序执行的最小单元。JVM 的线程通常映射到操作系统线程(平台相关),由操作系统负责调度。线程有生命周期:NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED。理解这些状态有助于定位阻塞与死锁问题。

3 常见创建线程的方式

3.1 继承 Thread

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hello from Thread");
    }
}

public class Test {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start(); // 不能调用 run(),否则变成普通方法调用
    }
}

3.2 实现 Runnable

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Hello from Runnable");
    }
}

public class Test {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

3.3 CallableFuture(有返回值/可抛异常)

import java.util.concurrent.*;

Callable<Integer> task = () -> {
    Thread.sleep(100);
    return 42;
};

ExecutorService exec = Executors.newSingleThreadExecutor();
Future<Integer> f = exec.submit(task);
try {
    Integer result = f.get(); // 阻塞等待结果
    System.out.println(result);
} finally {
    exec.shutdown();
}

4 更现代的并发:线程池与异步任务

直接 new Thread()通常不是好选择,推荐使用 ExecutorService 和线程池来管理线程生命周期与复用资源。

ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> {
    // 业务逻辑
});
pool.shutdown();

Java 还提供 CompletableFuture,便于写链式异步逻辑、组合多个异步任务:

CompletableFuture.supplyAsync(() -> compute())
    .thenApply(result -> transform(result))
    .exceptionally(ex -> { /* 处理异常 */ return null; });

5 同步、可见性与线程安全

多线程主要问题来自共享可变状态。要保证线程安全,有两类工作:互斥与可见性。

5.1 互斥(Mutual Exclusion)

  • synchronized:方法或代码块级别的锁,简单易用。

  • ReentrantLock:更灵活(可中断锁、可尝试获取、可设超时)。

示例 synchronized

public class Counter {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
    public synchronized int get() {
        return count;
    }
}

5.2 可见性

  • volatile:保证写入对其它线程可见,适合标志位或单个变量的可见性,但不能做复合操作(如 count++)。

  • 原子类(AtomicInteger 等):提供原子操作,避免使用锁的场景下仍能保证一致性。

示例 AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;
public class SafeCounter {
    private AtomicInteger count = new AtomicInteger(0);
    public void increment() {
        count.incrementAndGet();
    }
    public int get() {
        return count.get();
    }
}

5.3 内存模型提示(JMM)

Java 内存模型(JMM)定义了操作在多线程间的可见性和重排序规则。synchronizedvolatile、Lock 和并发集合都涉及内存屏障(happens-before)语义,理解这些语义有助于避免难以复现的 bug。

6 常见并发工具与用法速查

  • ExecutorService / ThreadPoolExecutor:线程池管理。

  • ScheduledExecutorService:定时任务。

  • CountDownLatch:等待一组事件完成。

  • CyclicBarrier:多个线程相互等待到达某点。

  • Semaphore:限制同时访问资源的线程数量。

  • ConcurrentHashMap:高性能并发映射。

  • BlockingQueue(如 LinkedBlockingQueue):生产者-消费者模式。

  • ForkJoinPool:适合分治(Fork/Join)并行计算。

  • CompletableFuture:异步编排与组合。

7 常见陷阱与调试技巧

  • 不要吞掉中断:被阻塞的方法(如 sleepwaitjoinblocking queue)抛出 InterruptedException,应正确响应(通常是恢复中断或退出线程)。

  • 避免死锁:保证加锁顺序一致、尽量减少持有锁的时间、使用 tryLock 和超时策略。

  • 慎用 volatile:它保证可见性但不保证复合操作的原子性。

  • 避免忙等待(busy-waiting):使用阻塞队列或条件变量替代循环检查。

  • 使用线程dump(jstack)和日志:定位死锁或线程阻塞时非常有用。

  • 并发问题往往是时间相关:用压力测试、线程模糊测试(fuzzing)或并发单元测试工具检查。

8 实际开发中的最佳实践

  1. 优先使用高层并发工具(线程池、BlockingQueue、Concurrent Collections、CompletableFuture),避免直接管理线程。

  2. 尽量保持数据不可变(immutable 对象),这样能减少同步需求。

  3. 把共享可变状态局部化(ThreadLocal)或通过消息传递/队列来隔离。

  4. 合理配置线程池大小:CPU 密集型任务线程数 ≈ CPU 核数 + 1;IO 密集型可更大,但要结合实际监控调整。

  5. 处理中断与超时:为阻塞操作设置超时,处理 InterruptedException,避免线程无限挂起。

  6. 记录线程上下文(如采用 MDC 输出日志),排查问题时更方便。

  7. 测试并发:使用压力测试、单元并发测试库(如 JUnit + Executors)以及静态分析工具。

9 总结

Java 多线程既为程序性能与响应性带来提升,也引入了复杂性。掌握线程创建、线程池与标准并发工具,理解同步与内存可见性原则,并遵循工程实践(优先高层工具、合理配置线程池、避免共享可变状态),可以把多线程风险降到最低。实践中应结合日志、线程 dump 与测试来稳步推进并发代码的可靠性。

Logo

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

更多推荐