目录
- 为什么需要并发编程?
- Java 线程基础
- 什么是线程?
- 线程 vs. 进程
- 如何创建和启动线程?
- 线程的生命周期
- 线程的核心问题:可见性与原子性
- 可见性
- 原子性
- 有序性
- Java 并发解决方案
synchronized关键字volatile关键字java.util.concurrent.atomic包
- 高级并发工具
- 线程池 (
java.util.concurrent.ExecutorService) - 锁 (
java.util.concurrent.locks.Lock) - 并发集合 (
java.util.concurrent.ConcurrentHashMap,CopyOnWriteArrayList等) - 同步工具类 (
CountDownLatch,CyclicBarrier,Semaphore等)
- 线程池 (
- 线程安全与最佳实践
为什么需要并发编程?
- 提高 CPU 利用率:现代 CPU 都是多核的,单线程程序无法充分利用所有核心,导致资源浪费,并发编程可以让多个线程在不同的核心上同时执行任务,从而提高程序的整体吞吐量。
- 提升程序响应速度:对于 I/O 密集型任务(如网络请求、文件读写),使用多线程可以在一个线程等待 I/O 的同时,让其他线程继续处理任务,避免整个程序被阻塞。
- 简化复杂模型:某些问题天然具有并行性,比如一个大型计算任务可以拆分成多个小任务并行处理。
Java 线程基础
什么是线程?
线程是 进程 中的一个执行单元,是 CPU 调度和分派的基本单位,一个进程可以包含多个线程,它们共享进程的内存空间和系统资源。

线程 vs. 进程
| 特性 | 进程 | 线程 |
|---|---|---|
| 资源 | 拥有独立的内存地址空间、文件句柄等。 | 共享所属进程的资源(内存、文件等)。 |
| 开销 | 创建、销毁、切换开销大。 | 创建、销毁、切换开销小。 |
| 通信 | 复杂,需要进程间通信。 | 简单,共享内存即可直接通信。 |
| 健壮性 | 一个进程崩溃,不影响其他进程。 | 一个线程崩溃(如导致内存错误),会导致整个进程崩溃。 |
如何创建和启动线程?
Java 提供了三种创建线程的方式:
继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
// 线程要执行的代码
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // 启动线程,调用 run() 方法
}
}
缺点:Java 是单继承的,继承了 Thread 类就无法再继承其他类。
实现 Runnable 接口(推荐)

class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
t1.start();
}
}
优点:
- 避免了单继承的限制。
- 将线程任务(
run方法的逻辑)与线程本身(Thread对象)分离,更符合面向设计原则。
实现 Callable 接口(有返回值)
Callable 是 Runnable 的增强版,它的 call() 方法可以有返回值,并且可以抛出异常。
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("Callable is running.");
return 123; // 可以返回一个结果
}
}
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
// 使用 FutureTask 来包装 Callable
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread t1 = new Thread(futureTask);
t1.start();
// 获取线程执行结果
Integer result = futureTask.get(); // get() 方法会阻塞,直到任务完成
System.out.println("Result from Callable: " + result);
}
}
线程的生命周期
一个线程从创建到销毁,会经历以下状态:
- NEW (新建):线程被创建,但尚未调用
start()方法。 - RUNNABLE (可运行):调用了
start()方法,此时线程可能正在运行,也可能在等待操作系统分配 CPU 时间片,在 Java API 中,它将Ready(就绪) 和Running(运行中) 两种状态合并。 - BLOCKED (阻塞):线程等待获取一个排他锁,以便进入同步块/同步方法,线程 A 已经锁住了对象,线程 B 也想进入被该对象锁住的同步块,此时线程 B 就会进入
BLOCKED状态。 - WAITING (等待):线程等待另一个线程来执行一个特定的操作,调用了
Object.wait(),Thread.join()或LockSupport.park(),这种状态会一直等待,直到其他线程显式地唤醒它。 - TIMED_WAITING (超时等待):和
WAITING类似,但它可以在指定的时间后自动醒来,调用了Thread.sleep(long),Object.wait(long),Thread.join(long)等。 - TERMINATED (终止):线程已经执行完毕或因异常而退出。
状态转换图示:
NEW -> start() -> RUNNABLE -> (获得 CPU) -> RUNNING -> (失去 CPU/主动让出) -> RUNNABLE
RUNNABLE -> (获取锁失败) -> BLOCKED -> (获得锁) -> RUNNABLE
RUNNABLE -> (调用 wait()/join() 等) -> WAITING / TIMED_WAITING -> (被唤醒/超时) -> RUNNABLE
RUNNABLE -> (执行结束) -> TERMINATED

线程的核心问题
当多个线程共享数据时,会引发三大问题:
可见性
问题:一个线程对共享变量的修改,对其他线程不可见。 原因:每个线程都有自己的工作内存(高速缓存),变量从主内存加载到工作内存,一个线程修改了变量,只是修改了自己工作内存中的副本,如果没有及时写回主内存,其他线程就看不到这个修改。 解决方案:
synchronized:在解锁时,会将工作内存中的变量刷新到主内存;在加锁时,会清空工作内存,从主内存重新加载变量。volatile:保证了每次读取变量都是从主内存加载,每次写入变量都会立即刷新到主内存。
原子性
问题:一个或多个操作,要么全部执行且执行的过程不会被任何因素打断,要么就都不执行。
经典案例:i++ 操作,它不是一个原子操作,包含三个步骤:1) 读取 i 的值;2) i 的值加 1;3) 将新值写回 i,在多线程环境下,这三步可能被交叉执行,导致结果错误。
解决方案:
synchronized:通过加锁,确保同一时间只有一个线程能进入临界区,保证了代码块的原子性。java.util.concurrent.atomic包:使用 CAS (Compare-And-Swap) 机制来实现原子操作,性能通常比synchronized更高。
有序性
问题:指令的执行顺序可能与代码编写的顺序不一致。 原因:为了优化性能,处理器和编译器可能会对指令进行重排序。 解决方案:
volatile:禁止指令重排序,保证了代码的执行顺序。synchronized:一个变量在同一个时刻只允许一条线程对其进行 lock 操作,这使得持有同一个锁的两个同步块只能串行地进入,从而保证了有序性。
Java 并发解决方案
synchronized 关键字
synchronized 是 Java 内置的重量级锁,是实现线程安全最基本、最常用的工具。
使用方式:
- 修饰实例方法:锁是当前对象实例 (
this)。public synchronized void instanceMethod() { ... } - 修饰静态方法:锁是当前类的
Class对象。public static synchronized void staticMethod() { ... } - 修饰代码块:可以指定锁对象,更加灵活。
public void someMethod() { synchronized (this) { // 锁是 this // 代码块 } }public void someMethod() { synchronized (MyClass.class) { // 锁是 MyClass 的 Class 对象 // 代码块 } }
volatile 关键字
volatile 是一个轻量级的同步机制,它保证了变量的 可见性 和 禁止指令重排序,但不保证 原子性。
适用场景:
-
状态标志位:一个线程修改标志位,其他线程读取。
private volatile boolean flag = false;
-
双重检查锁定 实现
singleton模式(DCL)。public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); } } } return instance; } }
java.util.concurrent.atomic 包
该包提供了一系列原子变量类,如 AtomicInteger, AtomicLong, AtomicReference 等,它们内部使用 CAS 算法,在不使用锁的情况下实现了原子操作,性能通常很高。
示例:
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
// 原子地执行加 1 操作
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
高级并发工具
线程池 (java.util.concurrent.ExecutorService)
频繁地创建和销毁线程是非常消耗资源的,线程池可以复用已创建的线程,减少开销,并控制最大并发数。
核心组件:
Executor:顶层接口。ExecutorService:扩展了Executor,添加了生命周期管理方法(shutdown(),submit()等)。ThreadPoolExecutor:最核心的线程池实现类。Executors:一个工厂类,提供了快速创建线程池的便捷方法。
常用线程池:
newFixedThreadPool(int nThreads):创建一个固定大小的线程池。newCachedThreadPool():创建一个可缓存的线程池,如果线程池大小超过了处理任务所需的线程,那么就会回收部分空闲线程。newSingleThreadExecutor():创建一个单线程的线程池,它只会用唯一的工作线程来执行任务。
使用示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小为 2 的线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}
锁 (java.util.concurrent.locks.Lock)
Lock 接口提供了比 synchronized 更广泛的锁定操作,最常用的实现是 ReentrantLock。
ReentrantLock vs. synchronized:
- 功能:
ReentrantLock提供了公平锁、可中断的锁尝试、尝试锁定(tryLock())等高级功能。 - 使用:
synchronized是 JVM 内置的,自动释放锁。ReentrantLock需要手动加锁 (lock()) 和解锁 (unlock()),通常在finally块中解锁,以确保锁一定会被释放。 - 性能:在竞争不激烈时,
ReentrantLock性能优于synchronized;在竞争激烈时,两者性能差不多。
示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private final Lock lock = new ReentrantLock();
public void perform() {
lock.lock(); // 加锁
try {
// 临界区代码
System.out.println("Performing a task in a thread-safe way.");
} finally {
lock.unlock(); // 解锁
}
}
}
并发集合
Java 提供了线程安全的集合类,位于 java.util.concurrent 包下。
ConcurrentHashMap:线程安全的HashMap,它使用分段锁(在 Java 8 中优化为 CAS +synchronized)来保证高并发下的性能。CopyOnWriteArrayList:线程安全的ArrayList,它的所有写操作(add, set, remove)都会在底层数组的副本上进行,写完后再将引用指向新数组,读操作则不加锁,性能很高。适用于读多写少的场景。BlockingQueue:阻塞队列,当队列为空时,获取元素的线程会阻塞;当队列满时,添加元素的线程会阻塞,常用于生产者-消费者模型。
同步工具类
CountDownLatch:倒计时门闩,允许一个或多个线程等待其他一组线程完成操作。// 等待 3 个任务线程执行完毕 CountDownLatch latch = new CountDownLatch(3); // 在每个任务线程完成时调用 latch.countDown() // 在主线程中调用 latch.await(),会阻塞,直到 countDown() 被调用 3 次
CyclicBarrier:循环屏障,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障,所有线程才会继续执行,可以重复使用。// 等待 4 个线程都到达屏障 CyclicBarrier barrier = new CyclicBarrier(4); // 每个线程执行到 barrier.await() 时会暂停 // 当第 4 个线程到达时,所有线程同时被唤醒
Semaphore:信号量,用于控制同时访问特定资源的线程数量。// 允许最多 3 个线程同时访问 Semaphore semaphore = new Semaphore(3); // 在访问资源前,调用 semaphore.acquire() // 在访问资源后,调用 semaphore.release()
线程安全与最佳实践
- 优先使用并发工具:优先使用
java.util.concurrent包中的工具,它们经过了充分测试,性能和可靠性都很好。 - 尽量减少同步范围:同步块(
synchronized或lock)内的代码应尽可能少,以减少锁竞争,提高并发度。 - 避免在同步块内调用外部方法:这可能导致死锁或性能问题(调用了另一个对象的同步方法)。
- 注意死锁:当两个或多个线程互相等待对方释放锁时,就会发生死锁,避免死锁的策略:
- 按照固定的顺序获取锁。
- 尽量使用
tryLock()而不是lock()。 - 使用
Lock的lockInterruptibly()方法,允许线程在等待时被中断。
- 理解
volatile的局限性:volatile不能保证复合操作的原子性,不要用它来替代synchronized或Atomic类来处理i++这样的操作。 - 优先使用线程池:避免手动创建线程,使用线程池来管理线程的生命周期和资源。
