【Java 线程安全、线程池与并发工具详解】
Java 线程安全、线程池与并发工具详解
一、Java 线程安全性
线程安全性是指多线程环境下,程序执行结果始终与单线程执行结果一致,且无数据竞争、死锁等问题。核心本质是“保证共享资源的原子性、可见性、有序性”。
1. 线程不安全的根源:共享资源竞争
当多个线程同时操作共享可变资源(如静态变量、堆内存中的对象属性)时,若缺乏同步控制,会导致以下问题:
- 原子性问题:一个操作(如
i++,实际包含“读取-修改-写入”三步)被拆分执行,中间被其他线程打断,导致结果错误。// 线程不安全示例:多线程执行 count++,结果可能小于预期 public class UnsafeCounter { private static int count = 0; public static void increment() { count++; // 非原子操作 } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) increment(); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) increment(); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); // 可能输出 1500(而非 2000) } } - 可见性问题:线程对共享变量的修改,其他线程无法及时感知(因 CPU 缓存、指令重排序导致)。
- 有序性问题:JVM 为优化性能对指令重排序,导致代码执行顺序与预期不一致(如“双重检查锁单例”的指令重排序问题)。
2. 保证线程安全的 5 种核心方案
(1)使用同步锁:synchronized
synchronized 是 Java 内置的隐式锁,可修饰方法或代码块,保证同一时间只有一个线程进入同步区域,实现原子性、可见性、有序性。
// 修复上述计数器:用 synchronized 修饰方法
public class SafeCounterWithSynchronized {
private static int count = 0;
// 同步静态方法(锁为类对象)
public static synchronized void increment() {
count++;
}
// 或同步代码块(锁为指定对象)
public static void incrementBlock() {
synchronized (SafeCounterWithSynchronized.class) {
count++;
}
}
}
(2)使用显式锁:Lock
java.util.concurrent.locks.Lock 接口(如 ReentrantLock)是显式锁,需手动调用 lock() 获取锁、unlock() 释放锁,功能比 synchronized 更灵活(如可中断锁、超时锁、公平锁)。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SafeCounterWithLock {
private static int count = 0;
private static final Lock lock = new ReentrantLock(); // 可重入锁
public static void increment() {
lock.lock(); // 获取锁
try {
count++; // 临界区操作
} finally {
lock.unlock(); // 释放锁(必须在 finally 中,避免锁泄漏)
}
}
}
(3)使用原子类:java.util.concurrent.atomic
原子类基于“CAS(Compare and Swap,比较并交换)”机制实现无锁原子操作,性能优于锁(避免线程上下文切换),常用类包括 AtomicInteger、AtomicLong、AtomicReference。
import java.util.concurrent.atomic.AtomicInteger;
public class SafeCounterWithAtomic {
// 原子整数,count++ 操作原子化
private static final AtomicInteger count = new AtomicInteger(0);
public static void increment() {
count.incrementAndGet(); // 原子操作:自增并返回新值
}
public static void main(String[] args) throws InterruptedException {
// 多线程执行后,结果必为 2000
Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) increment(); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) increment(); });
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(count.get()); // 输出 2000
}
}
(4)使用 volatile 关键字
volatile 是轻量级同步机制,无法保证原子性,但可保证可见性(线程修改后立即刷新到主内存,其他线程读取时从主内存获取)和有序性(禁止指令重排序),适用于“单线程写、多线程读”的场景(如状态标记)。
// volatile 示例:线程安全的状态标记
public class VolatileDemo {
// 用 volatile 修饰状态变量,保证可见性
private static volatile boolean isStop = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!isStop) {
// 执行任务
}
System.out.println("线程停止");
}).start();
// 主线程修改状态,子线程可立即感知
Thread.sleep(1000);
isStop = true;
}
}
(5)无共享资源:ThreadLocal
ThreadLocal 为每个线程提供独立的变量副本,避免线程间共享资源,从根本上消除竞争。适用于“每个线程需要独立实例”的场景(如数据库连接、Session 管理)。
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadLocalDemo {
// 每个线程拥有独立的 SimpleDateFormat 实例(避免线程不安全)
private static final ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() ->
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
public static String formatDate(Date date) {
// 每个线程调用 get() 时获取自己的副本
return sdf.get().format(date);
}
public static void main(String[] args) {
new Thread(() -> System.out.println(formatDate(new Date()))).start();
new Thread(() -> System.out.println(formatDate(new Date()))).start();
}
}
二、Java 线程池:原理与使用
线程池是“管理线程的容器”,通过复用线程减少线程创建/销毁的开销,控制并发数,避免资源耗尽,是生产环境中管理线程的首选方案。
1. 线程池核心参数(ThreadPoolExecutor)
Java 线程池的核心实现类是 java.util.concurrent.ThreadPoolExecutor,其构造方法包含 7 个核心参数,决定线程池的行为:
// ThreadPoolExecutor 构造方法
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数:线程池长期维持的线程数(即使空闲也不销毁)
int maximumPoolSize, // 最大线程数:线程池可创建的最大线程数
long keepAliveTime, // 非核心线程空闲存活时间:超过该时间则销毁
TimeUnit unit, // keepAliveTime 的时间单位(如 TimeUnit.SECONDS)
BlockingQueue<Runnable> workQueue, // 任务阻塞队列:核心线程满时,任务暂存的队列
ThreadFactory threadFactory, // 线程工厂:创建线程的方式(如自定义线程名)
RejectedExecutionHandler handler // 拒绝策略:任务队列满且线程数达最大值时的处理方式
) { ... }
2. 线程池工作流程
- 提交任务时,若核心线程数未达
corePoolSize,创建核心线程执行任务; - 若核心线程满,将任务加入
workQueue队列; - 若队列已满,且当前线程数 <
maximumPoolSize,创建非核心线程执行任务; - 若队列已满且线程数达
maximumPoolSize,触发拒绝策略处理任务。
3. 常用拒绝策略
| 拒绝策略类 | 作用 |
|---|---|
AbortPolicy(默认) |
直接抛出 RejectedExecutionException 异常,阻止系统正常运行 |
CallerRunsPolicy |
让提交任务的线程(如主线程)自己执行任务,减缓任务提交速度 |
DiscardPolicy |
直接丢弃新任务,不抛出异常 |
DiscardOldestPolicy |
丢弃队列中最旧的任务,尝试提交新任务 |
4. 线程池的创建方式
(1)通过 Executors 工具类(简单但不推荐生产环境)
Executors 提供了快捷创建线程池的方法,但可能因参数固定导致资源问题(如 newCachedThreadPool 可能创建无限线程):
// 1. 固定大小线程池(corePoolSize = maximumPoolSize)
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// 2. 单线程池(仅1个线程,任务串行执行)
ExecutorService singlePool = Executors.newSingleThreadExecutor();
// 3. 可缓存线程池(核心线程0,最大线程无限,适合短期任务)
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 4. 定时任务线程池(支持延迟/周期性任务)
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
(2)直接创建 ThreadPoolExecutor(推荐,可控性强)
根据业务需求自定义参数,避免资源耗尽风险:
// 示例:创建适合 IO 密集型任务的线程池(线程数可略多)
ExecutorService customPool = new ThreadPoolExecutor(
5, // 核心线程数:5
10, // 最大线程数:10
60, // 非核心线程空闲60秒后销毁
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 任务队列容量100
Executors.defaultThreadFactory(), // 默认线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:提交者执行
);
5. 线程池的关闭
shutdown():拒绝新任务,等待已提交任务完成后关闭。shutdownNow():立即尝试停止所有任务(可能导致任务中断),返回未执行的任务列表。
// 推荐关闭方式
customPool.shutdown();
if (!customPool.awaitTermination(60, TimeUnit.SECONDS)) {
// 超时后强制关闭
customPool.shutdownNow();
}
三、Java 并发集合容器
普通集合(如 ArrayList、HashMap)在多线程环境下不安全(如 ConcurrentModificationException),Java 提供了专门的并发集合,通过锁或无锁机制保证线程安全,常用容器如下:
| 并发集合类 | 对应普通集合 | 线程安全实现方式 | 适用场景 |
|---|---|---|---|
ConcurrentHashMap |
HashMap |
分段锁(JDK7)/ CAS + synchronized(JDK8+) | 高并发下的键值对存储,支持高效读写 |
CopyOnWriteArrayList |
ArrayList |
写时复制(修改时复制新数组,读不加锁) | 读多写少场景(如配置列表、事件监听器) |
CopyOnWriteArraySet |
HashSet |
基于 CopyOnWriteArrayList 实现 |
读多写少的无序集合 |
ConcurrentLinkedQueue |
LinkedList(队列) |
无锁 CAS 操作 | 高并发下的队列(FIFO),如任务队列 |
BlockingQueue 实现类 |
- | 内置锁或 Condition 机制 | 生产者-消费者模型(如线程池任务队列) |
典型示例:ConcurrentHashMap
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentMapDemo {
public static void main(String[] args) {
Map<String, Integer> map = new ConcurrentHashMap<>();
// 多线程并发写入
for (int i = 0; i < 10; i++) {
int num = i;
new Thread(() -> map.put("key" + num, num)).start();
}
// 多线程并发读取
new Thread(() -> {
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}).start();
}
}
四、原子操作与 CAS 机制
原子操作是“不可中断的操作”,要么完全执行,要么完全不执行。Java 中原子操作的实现依赖CAS(Compare and Swap,比较并交换) 机制,无需锁即可保证线程安全。
1. CAS 原理
CAS 包含 3 个操作数:
- 内存地址 V:存储变量的内存地址;
- 预期值 A:线程读取到的变量当前值;
- 新值 B:线程要写入的新值。
执行逻辑:若内存地址 V 中的值等于预期值 A,则将其更新为 B;否则不执行更新,线程可重试或放弃(无锁自旋)。
2. 原子类的使用(见“线程安全”部分)
除 AtomicInteger 外,Java 还提供:
- 引用类型原子类:
AtomicReference(原子更新对象引用); - 数组类型原子类:
AtomicIntegerArray(原子更新数组元素); - 字段更新器:
AtomicIntegerFieldUpdater(原子更新对象的某个字段)。
3. CAS 的优缺点
- 优点:无锁操作,避免线程上下文切换和锁竞争的开销,性能高;
- 缺点:
- ABA 问题:变量从 A 变为 B 再变回 A,CAS 会误认为未修改(可通过
AtomicStampedReference加版本号解决); - 自旋开销:高并发下 CAS 失败重试,可能导致 CPU 占用过高;
- 只能保证单个变量的原子操作:多个变量的原子性需结合锁实现。
- ABA 问题:变量从 A 变为 B 再变回 A,CAS 会误认为未修改(可通过
总结
Java 线程模型是并发编程的核心,理解进程与线程的区别、掌握线程安全保障机制(锁、原子类、ThreadLocal)、合理使用线程池和并发集合,是编写高效、安全的多线程程序的基础。实际开发中需根据业务场景选择合适的工具(如 IO 密集型任务用较大线程池,CPU 密集型任务用较小线程池),并警惕死锁、内存可见性等问题。
更多推荐



所有评论(0)