1. 线程安全与并发编程基础
在当今多核处理器普及的时代,编写线程安全的代码已成为开发者必备的核心技能。线程安全指的是当多个线程同时访问某个共享资源时,无论这些线程以何种方式调度执行,程序都能表现出正确的行为。我曾在电商系统开发中深刻体会到,一个未考虑线程安全的库存扣减逻辑,在促销活动时会导致严重的超卖问题。
1.1 线程安全问题的本质
线程不安全问题的根源在于三个特性:
- 原子性破坏:比如简单的count++操作,实际包含读取-修改-写入三个步骤,中间可能被其他线程打断
- 可见性问题:由于CPU缓存的存在,线程A修改的变量可能不会立即被线程B看到
- 指令重排序:编译器和处理器会优化指令执行顺序,可能导致意想不到的结果
java复制// 典型线程不安全示例
class Counter {
private int count = 0;
public void increment() {
count++; // 非原子操作
}
}
1.2 并发与并行的区别
这两个概念经常被混淆,但有着本质区别:
- 并发(Concurrency):指系统能够处理多个任务的能力,这些任务在逻辑上同时进行(可能交替执行)
- 并行(Parallelism):指系统真正同时执行多个任务(需要多核CPU支持)
重要提示:所有并行系统都是并发的,但并发系统不一定能实现并行。单核CPU通过时间片轮转实现并发,但无法真正并行。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 线程同步的三种核心方式
当多个线程需要访问共享资源时,必须通过同步机制来协调它们的操作。根据不同的场景需求,主要有三种实现方式。
2.1 synchronized 关键字
这是Java最基本的同步机制,可以用于:
- 实例方法:锁是当前对象实例
- 静态方法:锁是当前类的Class对象
- 代码块:可指定锁对象
java复制// 同步方法示例
public synchronized void transfer(Account target, int amount) {
this.balance -= amount;
target.balance += amount;
}
// 同步块示例
public void update() {
synchronized(lockObject) {
// 临界区代码
}
}
性能考虑:
- JDK6后synchronized进行了重大优化(偏向锁、轻量级锁、锁消除等)
- 在低竞争场景下性能已接近CAS操作
- 仍不适合极高并发的场景
2.2 ReentrantLock 显式锁
相比synchronized,ReentrantLock提供了更灵活的锁机制:
- 可中断的获取锁
- 超时获取锁
- 公平锁与非公平锁选择
- 支持多个条件变量
java复制private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 必须手动释放
}
}
选型建议:
- 需要高级功能(如超时、中断)时使用
- 竞争激烈时性能通常优于synchronized
- 别忘了在finally块中释放锁!
2.3 volatile 关键字的误区
volatile经常被误用于线程同步,但实际上它:
- 只能保证可见性(立即写入主存)
- 不能保证复合操作的原子性
- 适合作为状态标志位使用
java复制// 正确使用场景
volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
常见陷阱:误以为volatile可以替代锁。实际上像i++这样的操作,即使i是volatile的,仍然需要同步。
3. 线程池深度解析
直接创建线程的方式在高并发场景下会导致严重问题(资源耗尽、创建销毁开销大)。线程池通过复用线程解决了这些问题。
3.1 线程池的七大核心参数
理解这些参数是正确使用线程池的关键:
| 参数 | 说明 | 设置建议 |
|---|---|---|
| corePoolSize | 核心线程数 | CPU密集型:N+1 IO密集型:2N |
| maximumPoolSize | 最大线程数 | 根据业务峰值设置 |
| keepAliveTime | 空闲线程存活时间 | 根据业务特点调整 |
| unit | 时间单位 | 通常用TimeUnit.SECONDS |
| workQueue | 任务队列 | 有界队列防止OOM |
| threadFactory | 线程工厂 | 可自定义线程命名 |
| handler | 拒绝策略 | 根据业务需求选择 |
3.2 两种创建线程池的方式
3.2.1 通过Executors工厂方法(不推荐生产环境使用)
java复制// 固定大小线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(10);
// 缓存线程池(无界)
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 单线程池(保证顺序执行)
ExecutorService singleThread = Executors.newSingleThreadExecutor();
问题:
- FixedThreadPool和SingleThreadPool使用无界队列,可能导致OOM
- CachedThreadPool最大线程数为Integer.MAX_VALUE,可能创建过多线程
3.2.2 手动创建ThreadPoolExecutor(推荐)
java复制ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60, // 空闲时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
关键配置技巧:
- 队列容量需要根据业务吞吐量和延迟要求平衡
- 监控队列堆积情况调整参数
- 拒绝策略选择:
- AbortPolicy:直接抛出异常(默认)
- CallerRunsPolicy:由调用线程执行
- DiscardPolicy:静默丢弃
- DiscardOldestPolicy:丢弃队列最老任务
3.3 处理Runnable与Callable任务
3.3.1 Runnable任务
java复制executor.execute(() -> {
System.out.println("执行Runnable任务");
});
3.3.2 Callable任务及结果获取
java复制Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "任务结果";
});
// 获取结果(阻塞)
String result = future.get();
// 带超时的获取
try {
result = future.get(500, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true); // 取消任务
}
性能陷阱:
- 不当的future.get()调用会导致线程阻塞
- 批量任务建议使用CompletionService
- 考虑使用Guava的ListenableFuture或CompletableFuture
4. 高并发实践与性能优化
4.1 线程池大小配置公式
通用公式:
- CPU密集型:Ncpu + 1
- IO密集型:Ncpu × (1 + WT/ST)
其中:
- Ncpu = Runtime.getRuntime().availableProcessors()
- WT = 平均等待时间
- ST = 平均服务时间
实际案例:
在数据库连接池配置中,我们通过以下方式确定最优大小:
- 使用JMeter进行压力测试
- 监控数据库连接等待时间
- 逐步调整直到吞吐量不再明显提升
4.2 常见并发模式实现
4.2.1 生产者-消费者模式
java复制BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
// 生产者
executor.execute(() -> {
while (true) {
Task task = produceTask();
queue.put(task); // 阻塞直到有空间
}
});
// 消费者
executor.execute(() -> {
while (true) {
Task task = queue.take(); // 阻塞直到有任务
processTask(task);
}
});
4.2.2 Fork/Join框架
适合可分解的并行任务:
java复制class FibonacciTask extends RecursiveTask<Integer> {
final int n;
FibonacciTask(int n) { this.n = n; }
protected Integer compute() {
if (n <= 1) return n;
FibonacciTask f1 = new FibonacciTask(n - 1);
f1.fork();
FibonacciTask f2 = new FibonacciTask(n - 2);
return f2.compute() + f1.join();
}
}
4.3 并发编程陷阱与解决方案
死锁预防:
- 按固定顺序获取锁
- 使用tryLock()带超时机制
- 通过工具检测(如jstack)
资源竞争优化:
- 减小锁粒度(如ConcurrentHashMap的分段锁)
- 使用读写锁(ReentrantReadWriteLock)
- 考虑无锁数据结构(如AtomicInteger)
上下文切换开销:
- 避免过度创建线程
- 使用线程本地存储(ThreadLocal)
- 考虑协程(如Kotlin协程/Quasar)
5. 现代并发编程新趋势
5.1 Java虚拟线程(Loom项目)
Java19引入的虚拟线程特点:
- 轻量级(内存占用约2KB)
- 由JVM调度,不绑定OS线程
- 适合IO密集型任务
java复制try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
5.2 响应式编程(Reactive Streams)
通过Project Reactor实现背压控制:
java复制Flux.range(1, 100)
.parallel()
.runOn(Schedulers.parallel())
.map(i -> compute(i))
.sequential()
.subscribe(System.out::println);
5.3 结构化并发(Java21预览)
将相关任务视为一个工作单元:
java复制try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> findUser());
Future<Integer> order = scope.fork(() -> fetchOrder());
scope.join(); // 等待所有任务
scope.throwIfFailed(); // 如果有失败则抛出异常
return new Response(user.resultNow(), order.resultNow());
}
在实际项目中,我逐渐形成了这样的线程使用原则:
- 优先考虑无锁设计
- 必须加锁时尽量减小临界区
- 线程池配置参数要通过压测确定
- 高并发系统要实施完善的监控(线程数、队列长度等)
- 定期检查是否有线程泄漏
对于新项目,建议直接采用现代并发方案(如虚拟线程),而传统线程池技术仍然是面试和底层系统开发必须掌握的硬核知识。
