1. Java多线程编程基础概念
1.1 线程与进程的本质区别
在操作系统层面,线程是比进程更轻量级的执行单元。每个Java程序启动时,JVM会为其创建一个主线程(main线程),而开发者可以在这个主线程基础上创建更多子线程。理解线程与进程的区别对设计高效并发程序至关重要:
-
资源分配:进程拥有独立的地址空间、文件描述符等系统资源,而同一进程内的多个线程共享这些资源。在Java中,这意味着所有线程可以访问相同的堆内存和静态变量。
-
上下文切换:线程切换只需保存程序计数器、栈指针等少量寄存器状态,而进程切换需要切换整个地址空间,开销大约是线程切换的10-30倍。
-
通信机制:进程间通信(IPC)需要通过管道、消息队列等复杂机制,而线程间可以直接读写共享内存(但需注意同步问题)。
提示:虽然线程更轻量,但创建过多线程(如数千个)仍会导致性能下降,因为线程调度本身也有开销。通常建议使用线程池管理线程生命周期。
1.2 Java线程的三种创建方式
1.2.1 继承Thread类
这是最基本的创建方式,适合简单的线程任务。但Java是单继承语言,这种方式会占用宝贵的继承机会:
java复制class SimpleThread extends Thread {
@Override
public void run() {
System.out.println("Thread ID: " + getId());
}
}
// 使用方式
new SimpleThread().start();
实际开发中需要注意:
- 直接调用run()方法不会启动新线程,只是在当前线程同步执行
- 每个Thread实例只能调用一次start(),重复调用会抛出IllegalThreadStateException
1.2.2 实现Runnable接口
更推荐的方式,因为:
- 避免单继承限制
- 更适合线程池等高级用法
- 任务与执行逻辑解耦
java复制class Task implements Runnable {
@Override
public void run() {
System.out.println("Running in thread: " + Thread.currentThread().getName());
}
}
// 使用方式
new Thread(new Task()).start();
// 或提交给线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(new Task());
1.2.3 使用Callable和Future
当需要获取线程执行结果时,这是最佳选择。Callable的call()方法可以返回值并抛出异常,比Runnable更灵活:
java复制Callable<Integer> computation = () -> {
TimeUnit.SECONDS.sleep(1);
return ThreadLocalRandom.current().nextInt(100);
};
FutureTask<Integer> future = new FutureTask<>(computation);
new Thread(future).start();
// 阻塞获取结果(实际开发中应设置超时)
Integer result = future.get();
System.out.println("Result: " + result);
经验:Future.get()会阻塞当前线程,在UI线程中调用会导致界面卡顿。Android开发中应使用AsyncTask等替代方案。
2. 线程生命周期与状态管理
2.1 完整的线程状态转换
Java线程在其生命周期中会经历多种状态,通过Thread.getState()可以获取当前状态:
mermaid复制stateDiagram-v2
[*] --> NEW
NEW --> RUNNABLE: start()
RUNNABLE --> BLOCKED: 等待synchronized锁
RUNNABLE --> WAITING: wait()/join()
RUNNABLE --> TIMED_WAITING: sleep(n)/wait(n)
BLOCKED --> RUNNABLE: 获取到锁
WAITING --> RUNNABLE: notify()/notifyAll()
TIMED_WAITING --> RUNNABLE: 超时/被唤醒
RUNNABLE --> TERMINATED: run()结束
各状态详细说明:
- NEW:Thread对象已创建但未调用start()
- RUNNABLE:可运行状态(包括正在运行和就绪)
- BLOCKED:等待获取监视器锁(进入synchronized块)
- WAITING:无限期等待其他线程显式唤醒
- TIMED_WAITING:有限时间的等待
- TERMINATED:线程执行完毕
2.2 关键状态转换方法
2.2.1 sleep() vs wait()
这两个方法经常被混淆,但有着本质区别:
| 特性 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread静态方法 | Object实例方法 |
| 释放锁 | 不会 | 会 |
| 唤醒条件 | 时间到期 | notify()/notifyAll() |
| 使用场景 | 定时暂停 | 线程间协调 |
java复制// 典型wait/notify使用模式
synchronized (lock) {
while (!condition) {
lock.wait(); // 释放锁并等待
}
// 条件满足后继续执行
}
// 另一个线程中
synchronized (lock) {
condition = true;
lock.notifyAll(); // 唤醒所有等待线程
}
2.2.2 join()的深入理解
join()方法实现了一个线程等待另一个线程结束:
java复制Thread worker = new Thread(() -> {
// 模拟耗时任务
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
worker.start();
worker.join(3000); // 最多等待3秒
if (worker.isAlive()) {
System.out.println("Worker未在指定时间内完成");
} else {
System.out.println("Worker已完成");
}
避坑指南:join()会响应中断,如果等待线程被中断,会抛出InterruptedException。正确处理中断是健壮多线程程序的基础。
3. 线程同步与锁机制
3.1 synchronized的底层实现
synchronized关键字在JVM中的实现经历了多次优化:
- 偏向锁:Mark Word记录偏向线程ID,无竞争时无需同步
- 轻量级锁:通过CAS操作获取锁,适用于短时间锁定
- 重量级锁:真正的互斥锁,涉及操作系统互斥量
查看锁状态(需要JOL工具):
java复制import org.openjdk.jol.info.ClassLayout;
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
3.2 死锁的检测与预防
典型的死锁场景需要四个必要条件:
- 互斥条件
- 占有且等待
- 不可抢占
- 循环等待
检测死锁的几种方法:
- jstack工具:
bash复制jstack <pid> | grep -A 10 deadlock
- 编程检测:
java复制ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] threadIds = bean.findDeadlockedThreads();
if (threadIds != null) {
ThreadInfo[] infos = bean.getThreadInfo(threadIds);
for (ThreadInfo info : infos) {
System.out.println(info.getThreadName());
}
}
预防死锁的策略:
- 锁排序:按固定顺序获取锁
- 锁超时:tryLock(timeout)
- 使用更高级的并发工具(如ReentrantLock)
3.3 volatile关键字的正确理解
volatile解决的是可见性问题,但不保证原子性:
java复制class Counter {
private volatile int count = 0;
// 这个方法仍然不是线程安全的
public void increment() {
count++; // 实际上是读-改-写三步操作
}
}
volatile的典型使用场景:
- 状态标志位(如shutdownRequested)
- 单例模式的双重检查锁定
- 独立观察结果发布
4. JUC并发工具包详解
4.1 ReentrantLock的高级特性
相比synchronized,ReentrantLock提供更多灵活功能:
java复制ReentrantLock lock = new ReentrantLock(true); // 公平锁
Condition condition = lock.newCondition();
lock.lock();
try {
while (!conditionMet) {
condition.await(1, TimeUnit.SECONDS); // 可超时等待
}
// 临界区操作
condition.signalAll();
} finally {
lock.unlock(); // 必须在finally中释放
}
性能对比:
- 高竞争场景:ReentrantLock性能更好
- 低竞争场景:synchronized有JVM优化优势
4.2 CountDownLatch vs CyclicBarrier
两种常用的线程协调工具:
| 特性 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 重置 | 不可重用 | 可循环使用 |
| 计数方向 | 递减 | 递增 |
| 主要用途 | 等待事件完成 | 线程相互等待 |
| 异常处理 | 不影响其他线程 | 会传播异常给所有线程 |
CyclicBarrier示例:
java复制CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程已就位");
});
IntStream.range(0, 3).forEach(i -> new Thread(() -> {
try {
System.out.println("线程" + i + "准备");
barrier.await();
System.out.println("线程" + i + "继续");
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}).start());
4.3 CompletableFuture异步编程
Java 8引入的现代化异步编程工具:
java复制CompletableFuture.supplyAsync(() -> {
// 模拟耗时计算
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return 42;
}).thenApplyAsync(result -> result * 2)
.thenAccept(System.out::println)
.exceptionally(ex -> {
System.err.println("Error: " + ex.getMessage());
return null;
});
// 组合多个Future
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 2);
future1.thenCombine(future2, Integer::sum)
.thenAccept(System.out::println);
5. 线程池最佳实践
5.1 线程池参数详解
ThreadPoolExecutor的核心参数:
java复制public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
常见工作队列对比:
| 队列类型 | 特性 | 适用场景 |
|---|---|---|
| SynchronousQueue | 不存储任务,直接移交 | 高吞吐量,短任务 |
| LinkedBlockingQueue | 无界队列(默认Integer.MAX_VALUE) | 任务执行速度较慢 |
| ArrayBlockingQueue | 有界队列 | 防止资源耗尽 |
| PriorityBlockingQueue | 优先级队列 | 任务有优先级差异 |
5.2 合理配置线程池
计算密集型 vs IO密集型:
- 计算密集型:线程数 ≈ CPU核心数(+1应对突发)
- IO密集型:线程数 ≈ CPU核心数 × (1 + 平均等待时间/平均计算时间)
实际案例:Web服务器线程池配置
java复制int cpuCores = Runtime.getRuntime().availableProcessors();
int poolSize = cpuCores * 2; // 假设IO等待时间与计算时间相当
ExecutorService executor = new ThreadPoolExecutor(
poolSize,
poolSize * 2,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new NamedThreadFactory("web-worker"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
5.3 线程池监控与调优
监控关键指标:
java复制ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
// 定期采集指标
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleAtFixedRate(() -> {
System.out.println("活跃线程: " + executor.getActiveCount());
System.out.println("完成任务: " + executor.getCompletedTaskCount());
System.out.println("队列大小: " + executor.getQueue().size());
}, 0, 1, TimeUnit.SECONDS);
常见问题排查:
- 线程泄漏:任务执行时间过长或无限循环
- 队列堆积:任务产生速度 > 处理速度
- CPU利用率低:IO等待时间过长,需要增加线程数
- 频繁Full GC:任务对象太大,队列设置过长
6. 并发编程实战技巧
6.1 ThreadLocal的正确使用
ThreadLocal实现线程封闭的典型用法:
java复制private static final ThreadLocal<SimpleDateFormat> dateFormatHolder =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String formatDate(Date date) {
return dateFormatHolder.get().format(date);
}
内存泄漏风险:
- 线程池中的线程会复用,如果不清理ThreadLocal变量,可能导致:
- ThreadLocal对象无法被回收
- 关联的value对象无法被回收
解决方案:
java复制try {
// 使用ThreadLocal
} finally {
dateFormatHolder.remove(); // 必须清理
}
6.2 并发集合的选择
Java并发包提供的线程安全集合:
| 接口 | 非线程安全实现 | 线程安全实现 | 特点 |
|---|---|---|---|
| List | ArrayList | CopyOnWriteArrayList | 读多写少场景 |
| Set | HashSet | ConcurrentSkipListSet | 有序集合 |
| Map | HashMap | ConcurrentHashMap | 高并发读写 |
| Queue | LinkedList | LinkedBlockingQueue | 阻塞队列 |
ConcurrentHashMap优化技巧:
java复制ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>(32, 0.75f, 32);
// 原子更新
map.compute("key", (k, v) -> v == null ? 1L : v + 1);
// 批量操作
map.search(2, (k, v) -> v > 100 ? k : null);
6.3 性能优化实战
案例:并行处理百万级数据
java复制// 不好的实现 - 顺序处理
List<Data> results = bigList.stream()
.map(this::processItem)
.collect(Collectors.toList());
// 优化实现 - 并行处理
List<Data> results = bigList.parallelStream()
.map(this::processItem)
.collect(Collectors.toList());
// 更精细控制 - 使用ForkJoinPool
ForkJoinPool customPool = new ForkJoinPool(8);
try {
List<Data> results = customPool.submit(() ->
bigList.parallelStream()
.map(this::processItem)
.collect(Collectors.toList())
).get();
} finally {
customPool.shutdown();
}
性能对比指标:
| 数据规模 | 顺序处理(ms) | 并行处理(ms) | 加速比 |
|---|---|---|---|
| 10,000 | 120 | 45 | 2.7x |
| 100,000 | 1,100 | 320 | 3.4x |
| 1,000,000 | 11,200 | 2,850 | 3.9x |
经验法则:当单个任务处理时间超过100μs时,才考虑使用并行流。对于更细粒度的任务,线程切换开销可能抵消并行收益。