
文章目录
-
- 引言
- 第一部分:多线程基础概念
-
- [1.1 进程与线程的定义](#1.1 进程与线程的定义)
- [1.2 并发与并行的本质区别](#1.2 并发与并行的本质区别)
- [1.3 Java程序运行原理与JVM的多线程特性](#1.3 Java程序运行原理与JVM的多线程特性)
- [1.4 多线程的应用场景](#1.4 多线程的应用场景)
- 第二部分:线程的创建与生命周期
-
- [2.1 创建线程的四种方式](#2.1 创建线程的四种方式)
-
- [2.1.1 继承Thread类](#2.1.1 继承Thread类)
- [2.1.2 实现Runnable接口](#2.1.2 实现Runnable接口)
- [2.1.3 实现Callable接口(有返回值)](#2.1.3 实现Callable接口(有返回值))
- [2.1.4 使用线程池(Executor框架)](#2.1.4 使用线程池(Executor框架))
- [2.2 线程生命周期与六种状态](#2.2 线程生命周期与六种状态)
- [2.3 start()与run()的区别](#2.3 start()与run()的区别)
- [2.4 线程的常用方法](#2.4 线程的常用方法)
-
- [2.4.1 sleep():线程休眠](#2.4.1 sleep():线程休眠)
- [2.4.2 join():线程合并](#2.4.2 join():线程合并)
- [2.4.3 yield():线程让步](#2.4.3 yield():线程让步)
- [2.4.4 interrupt():线程中断](#2.4.4 interrupt():线程中断)
- [2.5 优雅地停止线程](#2.5 优雅地停止线程)
- 第三部分:线程同步与线程安全
-
- [3.1 什么是线程安全?](#3.1 什么是线程安全?)
- [3.2 synchronized关键字](#3.2 synchronized关键字)
-
- [3.2.1 同步实例方法](#3.2.1 同步实例方法)
- [3.2.2 同步静态方法](#3.2.2 同步静态方法)
- [3.2.3 同步代码块](#3.2.3 同步代码块)
- [3.2.4 案例:售票系统](#3.2.4 案例:售票系统)
- [3.2.5 synchronized的原理](#3.2.5 synchronized的原理)
- [3.3 volatile关键字](#3.3 volatile关键字)
- [3.4 Lock接口与ReentrantLock](#3.4 Lock接口与ReentrantLock)
-
- [3.4.1 基本用法](#3.4.1 基本用法)
- [3.4.2 ReentrantLock的独特功能](#3.4.2 ReentrantLock的独特功能)
- [3.4.3 synchronized与ReentrantLock对比](#3.4.3 synchronized与ReentrantLock对比)
- [3.5 死锁及解决方案](#3.5 死锁及解决方案)
-
- [3.5.1 什么是死锁?](#3.5.1 什么是死锁?)
- [3.5.2 死锁示例](#3.5.2 死锁示例)
- [3.5.3 避免死锁的策略](#3.5.3 避免死锁的策略)
- [3.6 线程间通信](#3.6 线程间通信)
-
- [3.6.1 wait/notify机制](#3.6.1 wait/notify机制)
- [3.6.2 案例:交替打印](#3.6.2 案例:交替打印)
- [3.6.3 sleep()与wait()的区别](#3.6.3 sleep()与wait()的区别)
- [3.6.4 Condition接口](#3.6.4 Condition接口)
- 第四部分:JUC高级并发工具
-
- [4.1 线程池](#4.1 线程池)
-
- [4.1.1 为什么要用线程池?](#4.1.1 为什么要用线程池?)
- [4.1.2 ThreadPoolExecutor核心参数](#4.1.2 ThreadPoolExecutor核心参数)
- [4.1.3 线程池执行流程](#4.1.3 线程池执行流程)
- [4.1.4 线程池参数配置建议](#4.1.4 线程池参数配置建议)
- [4.1.5 线程池状态](#4.1.5 线程池状态)
- [4.2 原子类](#4.2 原子类)
- [4.3 并发集合](#4.3 并发集合)
-
- [4.3.1 ConcurrentHashMap的并发特性](#4.3.1 ConcurrentHashMap的并发特性)
- [4.3.2 CopyOnWriteArrayList原理](#4.3.2 CopyOnWriteArrayList原理)
- [4.4 同步工具类](#4.4 同步工具类)
-
- [4.4.1 CountDownLatch(倒计数器)](#4.4.1 CountDownLatch(倒计数器))
- [4.4.2 CyclicBarrier(循环屏障)](#4.4.2 CyclicBarrier(循环屏障))
- [4.4.3 Semaphore(信号量)](#4.4.3 Semaphore(信号量))
- [4.4.4 Exchanger(交换器)](#4.4.4 Exchanger(交换器))
- [4.5 ThreadLocal](#4.5 ThreadLocal)
-
- [4.5.1 基本用法](#4.5.1 基本用法)
- [4.5.2 应用场景](#4.5.2 应用场景)
- [4.5.3 内存泄漏问题](#4.5.3 内存泄漏问题)
- 第五部分:多线程最佳实践与常见问题
-
- [5.1 线程池使用规范](#5.1 线程池使用规范)
- [5.2 如何选择合适的锁机制?](#5.2 如何选择合适的锁机制?)
- [5.3 常见问题诊断](#5.3 常见问题诊断)
-
- [5.3.1 如何定位死锁?](#5.3.1 如何定位死锁?)
- [5.3.2 线程池阻塞问题](#5.3.2 线程池阻塞问题)
- [5.3.3 性能分析工具](#5.3.3 性能分析工具)
- [5.4 多线程设计原则](#5.4 多线程设计原则)
- 结语

引言
在当今多核处理器普及的时代,多线程编程已经成为Java开发者必须掌握的核心技能。无论是高并发的Web服务、大数据处理,还是实时响应式应用,多线程技术都扮演着至关重要的角色。然而,多线程编程也是一把双刃剑:运用得当可以大幅提升系统性能,使用不当则可能导致数据错乱、死锁甚至系统崩溃。
本文将从基础概念入手,循序渐进地深入Java多线程的核心原理,通过丰富的代码案例和实践经验,帮助读者全面掌握Java多线程编程技术。全文约9000字,建议配合实际编码练习,以达到最佳学习效果。
第一部分:多线程基础概念
1.1 进程与线程的定义
在操作系统中,进程是资源分配的基本单位,它是一个正在执行的程序,包含了代码、数据和系统资源。每个进程拥有独立的地址空间,进程之间相互隔离。
线程则是操作系统能够进行运算调度的最小单位,它包含在进程之中,共享进程的资源(如内存、文件等)。一个进程可以包含多个线程,这些线程可以并行执行不同的任务。
打个比方:如果把进程比作一个工厂,那么线程就是工厂里的工人。工厂(进程)提供场地和设备(资源),工人们(线程)共享这些资源协同完成工作。
1.2 并发与并行的本质区别
这是初学者最容易混淆的概念。简单来说:
- 并行 :多个任务在同一时刻真正同时执行。这需要多核CPU的支持,每个核心独立处理一个任务。
- 并发 :多个任务在同一时间段内交替执行。在单核CPU上,通过快速的任务切换(时间片轮转),让用户感觉任务在同时进行,但实际上任一时刻只有一个任务在执行。
形象理解:
- 并行:四个厨师各做一道菜,真正同时进行。
- 并发:一个厨师在煮汤、煎肉饼、切沙拉之间快速切换,看起来像同时在做多件事。
为什么这很重要?
理解并发与并行的区别有助于合理设计多线程程序。如果你的任务是CPU密集型(需要大量计算),那么多线程在单核CPU上不会带来性能提升,反而可能因线程切换增加开销。而如果是I/O密集型(等待网络、磁盘),即使单核也能通过并发提高资源利用率。
1.3 Java程序运行原理与JVM的多线程特性
很多人以为Java程序是单线程的,其实不然。当我们执行java命令启动一个Java程序时,JVM会启动多个线程,至少包括:
- 主线程 :执行
main方法的线程。 - 垃圾回收线程:负责内存回收的后台线程。
- 其他守护线程:如编译优化线程等。
面试高频题:JVM的启动是多线程的吗?
答案:是的,JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。
1.4 多线程的应用场景
多线程并非万能,但在以下场景中不可或缺:
- 服务器并发处理:Web服务器同时处理多个客户端请求。
- 响应式界面:教学控屏软件同时向多台电脑共享屏幕。
- 大数据处理:分析日志、统计海量数据。
- 异步任务:在不阻塞主线程的情况下执行耗时操作(如文件下载)。
第二部分:线程的创建与生命周期
2.1 创建线程的四种方式
2.1.1 继承Thread类
这是最直接的方式,但受限于Java的单继承特性。
java
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 正在执行");
}
}
public class Demo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // 启动新线程
}
}
优点 :可以直接使用Thread类中的方法,代码简单。
缺点:如果类已经有父类,就不能使用这种方法。
2.1.2 实现Runnable接口
更推荐的方式,因为Java支持接口的多实现。
java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 正在执行");
}
}
public class Demo {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
t1.start();
}
}
优点 :不影响类的继承关系,多个线程可以共享同一个Runnable实例。
缺点:不能直接使用Thread中的方法,需要先获取线程对象。
2.1.3 实现Callable接口(有返回值)
如果需要线程执行后返回结果,可以使用Callable。
java
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
public class Demo {
public static void main(String[] args) throws Exception {
FutureTask<Integer> task = new FutureTask<>(new MyCallable());
new Thread(task).start();
Integer result = task.get(); // 获取线程执行结果(会阻塞)
System.out.println("计算结果:" + result);
}
}
Callable与Runnable的区别:
- Runnable的
run()无返回值,不能抛出受检异常。 - Callable的
call()有返回值,可以抛出异常。
2.1.4 使用线程池(Executor框架)
这是企业级开发中最常用的方式,避免频繁创建销毁线程的开销。
java
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务");
});
}
executor.shutdown();
2.2 线程生命周期与六种状态
Java线程的生命周期由Thread.State枚举定义了6种状态:
NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED
| 状态 | 说明 |
|---|---|
| NEW | 线程刚创建,尚未调用start() |
| RUNNABLE | 可运行状态,包含就绪和运行两种子状态 |
| BLOCKED | 阻塞状态,等待获取监视器锁 |
| WAITING | 无限期等待,需被显式唤醒 |
| TIMED_WAITING | 限期等待,时间到自动唤醒 |
| TERMINATED | 线程执行完毕 |
可以通过Thread.getState()方法实时监控线程状态。
2.3 start()与run()的区别
这是面试中的经典问题。看下面的代码:
java
Thread t = new Thread(() -> System.out.println("线程执行"));
t.run(); // 不会启动新线程,只是普通方法调用
t.start(); // 启动新线程,JVM自动调用run()
核心区别:
start()会启动新线程,由新线程执行run()中的代码。run()只是普通方法调用,在调用者线程中执行,不会创建新线程。
2.4 线程的常用方法
2.4.1 sleep():线程休眠
让当前线程暂停执行指定时间,但不释放锁。
java
try {
Thread.sleep(2000); // 休眠2秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
}
关于sleep,有一个重要观点:谁执行不重要,重要的是在哪里执行 。无论通过Thread.sleep()还是t.sleep()调用,总是让当前正在执行的线程休眠,与调用对象无关。
2.4.2 join():线程合并
等待目标线程终止后再继续执行。
java
Thread t = new Thread(() -> {
// 执行耗时操作
});
t.start();
t.join(); // 当前线程等待t执行完毕
System.out.println("t已完成");
2.4.3 yield():线程让步
提示调度器当前线程愿意让出CPU,但调度器可能忽略。
2.4.4 interrupt():线程中断
中断线程,设置中断标志。但线程需要自己检查中断标志并响应。
2.5 优雅地停止线程
绝不推荐使用stop()方法 ,它已被废弃。因为stop()会强制终止线程,可能导致锁无法释放、数据不一致等问题。
正确方式:使用中断机制配合标志位。
java
class MyThread extends Thread {
private volatile boolean stopped = false;
@Override
public void run() {
while (!stopped && !Thread.currentThread().isInterrupted()) {
try {
// 执行任务
Thread.sleep(1000);
} catch (InterruptedException e) {
// 在阻塞时被中断,退出循环
break;
}
}
}
public void shutdown() {
stopped = true;
this.interrupt();
}
}
第三部分:线程同步与线程安全
3.1 什么是线程安全?
当多个线程访问共享资源时,如果没有采取同步措施,可能会导致数据不一致或错误的状态,这就是线程不安全。
经典问题:多个线程同时执行count++,最终结果可能小于预期,因为count++不是原子操作(包含读、加、写三步)。
3.2 synchronized关键字
synchronized是Java最基础的同步机制,确保同一时刻只有一个线程执行特定代码块。
3.2.1 同步实例方法
锁对象是当前实例(this)。
java
public synchronized void increment() {
count++;
}
3.2.2 同步静态方法
锁对象是类对象(ClassName.class)。
java
public static synchronized void staticMethod() { }
3.2.3 同步代码块
可以指定锁对象,粒度更细。
java
public void increment() {
synchronized (this) {
count++;
}
}
3.2.4 案例:售票系统
java
class TicketWindow implements Runnable {
private static int tickets = 100;
private Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
if (tickets > 0) {
try {
Thread.sleep(50); // 模拟出票耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " 卖出第 " + tickets-- + " 张票");
} else {
break;
}
}
}
}
}
关键点 :多个同步代码块必须使用同一个锁对象才能实现同步。
3.2.5 synchronized的原理
在字节码层面,同步代码块前后会有monitorenter和monitorexit指令。每个对象都有一个监视器锁(monitor),线程进入同步块前必须获得该锁,退出时释放。
3.3 volatile关键字
volatile是轻量级的同步机制,它保证:
- 可见性:一个线程修改变量后,其他线程立即看到最新值。
- 禁止指令重排序:阻止编译器对volatile变量前后的指令进行重排。
但它不保证原子性,适用于以下场景:
场景一:状态标记
java
class Worker {
private volatile boolean running = true;
public void run() {
while (running) {
// 执行任务
}
}
public void stop() {
running = false;
}
}
场景二:单例模式双检锁
java
class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // volatile防止指令重排
}
}
}
return instance;
}
}
3.4 Lock接口与ReentrantLock
java.util.concurrent.locks.Lock提供了比synchronized更灵活的锁操作。
3.4.1 基本用法
java
Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 必须释放锁
}
}
3.4.2 ReentrantLock的独特功能
| 功能 | 说明 |
|---|---|
| 可中断锁 | lock.lockInterruptibly()允许在等待锁时响应中断 |
| 可超时 | lock.tryLock(3, TimeUnit.SECONDS)等待锁最多3秒 |
| 公平锁 | new ReentrantLock(true)按请求顺序获取锁 |
| 多个条件变量 | 一个Lock可以创建多个Condition |
3.4.3 synchronized与ReentrantLock对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 使用方式 | 关键字,自动释放 | API,需手动解锁 |
| 锁类型 | 非公平锁 | 可设置公平/非公平 |
| 可中断 | 不支持 | 支持lockInterruptibly() |
| 超时 | 不支持 | 支持tryLock(timeout) |
| 条件变量 | 一个锁一个等待集 | 多个Condition |
| 性能 | JDK6后优化较好 | 类似 |
3.5 死锁及解决方案
3.5.1 什么是死锁?
死锁是多个线程相互等待对方持有的资源,导致所有线程都无法继续执行的情况。
死锁产生的四个必要条件:
- 互斥:资源一次只能被一个线程使用。
- 占有且等待:线程已持有一个资源,同时等待其他资源。
- 不可剥夺:资源只能由持有者主动释放。
- 循环等待:多个线程形成循环等待链。
3.5.2 死锁示例
java
class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lockA) {
System.out.println("线程1获得锁A");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lockB) {
System.out.println("线程1获得锁B");
}
}
}).start();
new Thread(() -> {
synchronized (lockB) {
System.out.println("线程2获得锁B");
synchronized (lockA) {
System.out.println("线程2获得锁A");
}
}
}).start();
}
}
3.5.3 避免死锁的策略
- 固定锁顺序:所有线程按相同顺序获取锁。
- 超时放弃 :使用
tryLock(timeout),获取不到就放弃。 - 减少锁的粒度:尽量缩小同步块的范围。
- 使用并发工具 :如
ConcurrentHashMap代替手动同步。
3.6 线程间通信
3.6.1 wait/notify机制
wait()和notify()是Object类的方法,用于线程间协作。它们必须在同步块中使用,且调用对象必须是同步锁对象。
为什么必须在同步块中调用?
- 避免
wait()和notify()之间的竞态条件。 - 确保调用线程持有该对象的锁。
3.6.2 案例:交替打印
java
class Printer {
private int flag = 1;
public void printNumbers() throws InterruptedException {
synchronized (this) {
while (flag != 1) {
this.wait();
}
System.out.print("1,2,3,4,5");
flag = 2;
this.notify(); // 唤醒等待的线程
}
}
public void printLetters() throws InterruptedException {
synchronized (this) {
while (flag != 2) {
this.wait();
}
System.out.print("A,B,C,D,E");
flag = 1;
this.notify();
}
}
}
注意 :wait()应在循环中检查条件,而不是if,防止虚假唤醒。
3.6.3 sleep()与wait()的区别
| 比较点 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread类 | Object类 |
| 是否释放锁 | 否 | 是 |
| 唤醒方式 | 时间到自动唤醒 | notify/notifyAll或超时 |
| 使用位置 | 任何地方 | 必须在同步块中 |
3.6.4 Condition接口
与Lock配合使用,一个Lock可以创建多个Condition,实现更精细的线程控制。
java
class BoundedBuffer {
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items = new Object[100];
private int putIndex, takeIndex, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
items[putIndex] = x;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
}
第四部分:JUC高级并发工具
4.1 线程池
4.1.1 为什么要用线程池?
频繁创建和销毁线程的开销很大,线程池通过复用线程来解决这个问题,同时还可以控制并发数量。
4.1.2 ThreadPoolExecutor核心参数
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize: 核心线程数
10, // maximumPoolSize: 最大线程数
60, TimeUnit.SECONDS, // keepAliveTime: 空闲线程存活时间
new LinkedBlockingQueue<>(100), // workQueue: 任务队列
Executors.defaultThreadFactory(), // threadFactory: 线程工厂
new ThreadPoolExecutor.AbortPolicy() // handler: 拒绝策略
);
4.1.3 线程池执行流程
- 如果线程数 < 核心线程数,创建新线程执行任务。
- 如果线程数 ≥ 核心线程数,任务加入队列等待。
- 如果队列已满且线程数 < 最大线程数,创建非核心线程执行任务。
- 如果队列已满且线程数 ≥ 最大线程数,执行拒绝策略。
拒绝策略:
AbortPolicy:抛出异常(默认)CallerRunsPolicy:调用者线程执行DiscardPolicy:静默丢弃DiscardOldestPolicy:丢弃最旧的任务
4.1.4 线程池参数配置建议
根据任务类型决定:
- CPU密集型(大量计算):线程数 ≈ CPU核心数 + 1
- IO密集型(等待网络/磁盘):线程数 ≈ CPU核心数 * 2
- 混合型:通过压测确定最优值
生产环境:核心线程数与最大线程数通常保持一致,避免频繁创建销毁线程。
4.1.5 线程池状态
- RUNNING:正常运行
- SHUTDOWN:不再接收新任务,处理完队列任务
- STOP:立即停止,中断正在执行的任务
- TIDYING:所有任务终止,准备调用
terminated() - TERMINATED:完全终止
4.2 原子类
java.util.concurrent.atomic包提供了一系列基于CAS(Compare and Swap)的原子类,如AtomicInteger、AtomicLong等。
java
AtomicInteger counter = new AtomicInteger(0);
// 多个线程同时调用,保证原子性
counter.incrementAndGet(); // 相当于 i++
CAS原理:比较当前内存值与预期值,如果相等则更新为新值,整个过程是原子的。CAS是一种无锁算法,性能优于锁。
4.3 并发集合
| 集合类 | 适用场景 |
|---|---|
ConcurrentHashMap |
高并发Map,替代Hashtable |
CopyOnWriteArrayList |
读多写少的List |
BlockingQueue |
生产者-消费者模式 |
ConcurrentLinkedQueue |
高并发队列 |
4.3.1 ConcurrentHashMap的并发特性
- 读操作无锁,写操作分段锁(Java 7)或CAS+synchronized(Java 8)。
- 迭代器弱一致性,不会抛出
ConcurrentModificationException。
4.3.2 CopyOnWriteArrayList原理
写入时复制:写操作先复制数组,修改后再将原数组引用指向新数组。读操作不加锁,适合读多写少的场景。
4.4 同步工具类
4.4.1 CountDownLatch(倒计数器)
让一个或多个线程等待其他线程完成操作。
java
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 执行任务
latch.countDown(); // 计数器减1
}).start();
}
latch.await(); // 等待计数器归零
System.out.println("所有任务完成");
4.4.2 CyclicBarrier(循环屏障)
多个线程互相等待,到达同一个屏障点后继续执行。
java
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达屏障,优先执行此任务");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 执行部分任务
barrier.await(); // 等待其他线程
// 继续执行
}).start();
}
与CountDownLatch的区别:CyclicBarrier可复用,CountDownLatch不可复用。
4.4.3 Semaphore(信号量)
控制同时访问资源的线程数量。
java
Semaphore semaphore = new Semaphore(3); // 最多3个线程同时访问
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
// 访问共享资源
Thread.sleep(1000);
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
4.4.4 Exchanger(交换器)
用于两个线程交换数据。
java
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
try {
String data = exchanger.exchange("来自线程1的数据");
System.out.println("线程1收到:" + data);
} catch (InterruptedException e) {}
}).start();
new Thread(() -> {
try {
String data = exchanger.exchange("来自线程2的数据");
System.out.println("线程2收到:" + data);
} catch (InterruptedException e) {}
}).start();
4.5 ThreadLocal
ThreadLocal为每个线程提供独立的变量副本,线程之间互不影响。
4.5.1 基本用法
java
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
// 在线程A中
threadLocal.set(100);
Integer value = threadLocal.get(); // 100
// 在线程B中
Integer value = threadLocal.get(); // 0 (独立副本)
4.5.2 应用场景
- 数据库连接管理(每个线程自己的Connection)
- 用户会话信息(Web应用)
- 事务上下文
4.5.3 内存泄漏问题
重要 :使用完ThreadLocal后,务必调用remove()方法,特别是在线程池环境中,否则可能因线程复用导致内存泄漏。
java
try {
threadLocal.set(value);
// 业务逻辑
} finally {
threadLocal.remove();
}
第五部分:多线程最佳实践与常见问题
5.1 线程池使用规范
阿里巴巴Java开发手册中关于线程池的规范:
【强制】 线程池不允许使用
Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让开发者更加明确线程池的运行规则,规避资源耗尽的风险。
Executors返回的线程池存在以下问题:
newFixedThreadPool:队列为LinkedBlockingQueue,容量无限,可能导致OOM。newCachedThreadPool:最大线程数无限,可能导致创建过多线程。
5.2 如何选择合适的锁机制?
| 场景 | 推荐方案 |
|---|---|
| 简单的同步方法 | synchronized |
| 需要公平锁 | ReentrantLock(true) |
| 需要尝试获取锁 | tryLock() |
| 读多写少的共享数据 | ReadWriteLock |
| 简单计数器 | AtomicInteger |
| 高并发Map | ConcurrentHashMap |
5.3 常见问题诊断
5.3.1 如何定位死锁?
- 使用
jstack命令查看线程堆栈。 - VisualVM等工具的可视化分析。
- 代码层面使用
tryLock加超时避免死锁。
5.3.2 线程池阻塞问题
- 队列过大导致内存压力。
- 核心线程数设置过小,任务处理缓慢。
- 任务内部死锁导致线程永久占用。
5.3.3 性能分析工具
- JConsole:监控线程数、锁竞争。
- VisualVM:分析CPU、内存、线程状态。
- JProfiler:深入定位性能瓶颈。
5.4 多线程设计原则
- 优先使用并发工具而非手动同步。
- 最小化锁范围:只在必要时同步。
- 避免在锁中执行耗时操作。
- 使用不可变对象,天然线程安全。
- 细粒度锁优于粗粒度锁。
- 警惕死锁,保持一致的锁顺序。
结语
多线程编程是Java开发中的深水区,既充满挑战也富有魅力。从基础的线程创建到高级的并发工具,从简单的同步机制到复杂的无锁算法,每一步都需要扎实的理论基础和丰富的实践经验。
本文系统地梳理了Java多线程的知识体系,涵盖了概念基础、线程控制、同步机制、并发工具以及最佳实践。希望读者能够结合实际项目,从简单的并发场景开始,逐步深入,最终掌握这门核心技能。
记住:多线程不是为了使用而使用,而是为了解决性能问题而使用的工具。在追求并发性能的同时,更要保证程序的正确性和可维护性。