在互联网应用架构中,高并发是绕不开的核心挑战。当多个线程同时操作共享资源时,若缺乏有效的同步机制,极易出现数据错乱、重复计算等线程安全问题,比如电商平台的库存超卖、支付系统的金额计算错误等。在 Java 生态中,synchronized 关键字与 java.util.concurrent.locks.Lock 接口是实现线程同步的两大核心方案。本文将深入剖析二者的底层原理、核心差异,并结合实际业务场景给出选型建议,搭配完整代码示例助力开发者快速落地。
一、线程安全的核心矛盾:共享资源的并发竞争
在深入技术细节前,我们先明确线程安全的本质。线程安全问题的根源在于“多个线程对共享资源的非原子操作”。例如,一个简单的库存扣减操作 stock--,看似是一行代码,实则包含“读取库存值→计算新值→写入库存值”三个步骤。在高并发下,多个线程会同时执行这三个步骤,导致库存值被错误覆盖。
举个具体场景:假设初始库存为 10,线程 A 读取到库存 10 后,CPU 时间片被线程 B 抢占,线程 B 也读取到库存 10 并完成扣减(库存变为 9);随后线程 A 恢复执行,基于之前读取的 10 完成扣减,最终库存变为 9 而非 8——这就是典型的“线程安全问题”。synchronized 与 Lock 的核心作用,就是通过“互斥机制”保证同一时间只有一个线程能执行临界区代码,从而避免并发竞争。
二、基础方案:synchronized 关键字的使用与原理
synchronized 是 Java 内置的同步机制,无需手动管理锁的获取与释放,使用简单、稳定性高,是初学者入门线程同步的首选。
2.1 synchronized 的核心特性
-
隐式锁机制:自动完成锁的获取(进入临界区时)与释放(退出临界区或发生异常时),无需开发者手动操作,降低了锁泄漏的风险。
-
锁的升级机制:为了平衡性能与安全性,
synchronized采用了“偏向锁→轻量级锁→重量级锁”的升级路径。在低并发场景下使用偏向锁/轻量级锁(基于 CAS 操作),避免操作系统内核态切换的开销;高并发竞争时升级为重量级锁,通过操作系统的互斥量保证线程安全。 -
可重入性:同一线程可以多次获取同一把锁,不会出现自己阻塞自己的死锁问题。例如,一个同步方法调用另一个同步方法,线程无需重新申请锁即可直接执行。
-
非公平锁:当锁被释放后,等待队列中的线程并非按照“先到先得”的顺序获取锁,而是由操作系统随机调度,这样可以减少线程切换的开销,但可能导致部分线程长期饥饿。
2.2 synchronized 的三种使用方式(代码示例)
synchronized 可作用于实例方法、静态方法和代码块,不同使用方式对应锁定的对象不同。
方式 1:作用于实例方法(锁定当前对象)
锁定的是当前类的实例对象,多个线程操作同一实例时会竞争锁,操作不同实例时互不影响。
/**
* 库存服务(synchronized 实例方法版)
*/
public class StockServiceSynchronized {
// 共享资源:库存数量
private int stock = 100;
// 同步实例方法:锁定当前 StockServiceSynchronized 实例
public synchronized void deductStock(int num) {
if (stock >= num) {
System.out.println(Thread.currentThread().getName() + " 开始扣减库存,当前库存:" + stock);
// 模拟业务耗时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock -= num;
System.out.println(Thread.currentThread().getName() + " 扣减库存成功,剩余库存:" + stock);
} else {
System.out.println(Thread.currentThread().getName() + " 扣减库存失败,库存不足");
}
}
public int getStock() {
return stock;
}
// 测试高并发场景
public static void main(String[] args) {
StockServiceSynchronized service = new StockServiceSynchronized();
// 启动 10 个线程同时扣减库存
for (int i = 0; i < 10; i++) {
new Thread(() -> service.deductStock(10), "线程-" + i).start();
}
}
}
方式 2:作用于静态方法(锁定类对象)
锁定的是当前类的 Class 对象,无论创建多少个实例,所有线程都会竞争同一把锁,适用于保护静态共享资源。
/**
* 库存服务(synchronized 静态方法版)
*/
public class StaticStockServiceSynchronized {
// 静态共享资源:总库存
private static int totalStock = 1000;
// 同步静态方法:锁定 StaticStockServiceSynchronized.class
public static synchronized void deductTotalStock(int num) {
if (totalStock >= num) {
System.out.println(Thread.currentThread().getName() + " 开始扣减总库存,当前总库存:" + totalStock);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
totalStock -= num;
System.out.println(Thread.currentThread().getName() + " 扣减总库存成功,剩余总库存:" + totalStock);
} else {
System.out.println(Thread.currentThread().getName() + " 扣减总库存失败,库存不足");
}
}
public static int getTotalStock() {
return totalStock;
}
public static void main(String[] args) {
// 即使创建多个实例,静态方法锁定的是类对象,仍会同步
for (int i = 0; i < 15; i++) {
new Thread(() -> StaticStockServiceSynchronized.deductTotalStock(50), "线程-" + i).start();
}
}
}
方式 3:作用于代码块(锁定指定对象)
通过 synchronized(锁定对象) 明确指定锁定的对象,灵活性更高,可仅对临界区代码加锁,减少锁的竞争范围。
/**
* 库存服务(synchronized 代码块版)
*/
public class BlockStockServiceSynchronized {
private int stock = 200;
// 自定义锁定对象(建议使用专用对象,避免与其他锁冲突)
private final Object lock = new Object();
public void deductStock(int num) {
// 非临界区代码(无需加锁,提高性能)
System.out.println(Thread.currentThread().getName() + " 发起库存扣减请求");
// 仅对临界区加锁,锁定自定义对象
synchronized (lock) {
if (stock >= num) {
System.out.println(Thread.currentThread().getName() + " 开始扣减库存,当前库存:" + stock);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock -= num;
System.out.println(Thread.currentThread().getName() + " 扣减库存成功,剩余库存:" + stock);
} else {
System.out.println(Thread.currentThread().getName() + " 扣减库存失败,库存不足");
}
}
// 非临界区代码(无需加锁)
System.out.println(Thread.currentThread().getName() + " 库存扣减请求结束");
}
public int getStock() {
return stock;
}
public static void main(String[] args) {
BlockStockServiceSynchronized service = new BlockStockServiceSynchronized();
for (int i = 0; i < 12; i++) {
new Thread(() -> service.deductStock(15), "线程-" + i).start();
}
}
}
三、进阶方案:Lock 接口的特性与实战
JDK 1.5 引入的 Lock 接口,是对 synchronized 的补充与增强。它提供了更灵活的锁操作机制,支持手动获取/释放锁、中断响应、超时获取锁等特性,满足复杂业务场景的需求。常用的实现类是 ReentrantLock(可重入锁)。
3.1 Lock 接口的核心优势
-
显式锁机制:需通过
lock()手动获取锁,通过unlock()手动释放锁,通常建议在finally块中执行unlock(),确保锁一定被释放,避免锁泄漏。 -
支持公平锁与非公平锁:
ReentrantLock构造函数可指定fair参数(默认非公平)。公平锁会按照线程等待的顺序分配锁,避免线程饥饿,但会增加线程切换开销,降低并发性能。 -
可中断锁:通过
lockInterruptibly()方法获取锁时,若线程被中断,会抛出InterruptedException,线程可响应中断并释放资源,避免死锁。 -
超时获取锁:通过
tryLock(long time, TimeUnit unit)方法设置超时时间,若在指定时间内未获取到锁则返回false,线程可自行处理,进一步降低死锁风险。 -
条件变量(Condition):支持通过
newCondition()方法创建多个条件变量,实现更精细的线程唤醒机制(如“生产者-消费者”模型中,可分别唤醒生产者或消费者线程),而synchronized仅能通过wait()/notify()唤醒随机线程或所有线程。
3.2 ReentrantLock 的代码示例(核心场景)
场景 1:基础同步(公平锁与非公平锁)
import java.util.concurrent.locks.ReentrantLock;
/**
* 库存服务(ReentrantLock 版)
*/
public class StockServiceLock {
private int stock = 150;
// 1. 非公平锁(默认):new ReentrantLock()
// 2. 公平锁:new ReentrantLock(true)
private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
public void deductStock(int num) {
// 手动获取锁
lock.lock();
try {
// 临界区代码
if (stock >= num) {
System.out.println(Thread.currentThread().getName() + " 开始扣减库存,当前库存:" + stock);
Thread.sleep(100);
stock -= num;
System.out.println(Thread.currentThread().getName() + " 扣减库存成功,剩余库存:" + stock);
} else {
System.out.println(Thread.currentThread().getName() + " 扣减库存失败,库存不足");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 手动释放锁(必须在 finally 中执行,确保锁释放)
lock.unlock();
}
}
public int getStock() {
return stock;
}
public static void main(String[] args) {
StockServiceLock service = new StockServiceLock();
// 公平锁下,线程会按启动顺序获取锁
for (int i = 0; i < 10; i++) {
new Thread(() -> service.deductStock(12), "线程-" + i).start();
}
}
}
场景 2:超时获取锁(避免死锁)
当多个线程竞争锁时,通过超时机制避免线程无限期等待,适用于对响应时间有要求的场景。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutLockExample {
private final ReentrantLock lock = new ReentrantLock();
private String resource = "初始资源";
public void operateResource(long timeout) {
try {
// 超时获取锁:timeout 时间内未获取到则返回 false
if (lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获取锁成功,操作资源:" + resource);
// 模拟资源操作耗时
Thread.sleep(500);
resource = Thread.currentThread().getName() + " 已修改资源";
System.out.println(Thread.currentThread().getName() + " 操作资源完成,新资源:" + resource);
} finally {
lock.unlock();
}
} else {
// 超时未获取到锁,执行降级逻辑
System.out.println(Thread.currentThread().getName() + " 超时未获取到锁,执行降级处理");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 获取锁时被中断,释放资源");
Thread.currentThread().interrupt(); // 保留中断状态
}
}
public static void main(String[] args) {
TimeoutLockExample example = new TimeoutLockExample();
// 线程 1 先获取锁,线程 2 超时未获取
new Thread(() -> example.operateResource(1000), "线程-1").start();
new Thread(() -> example.operateResource(300), "线程-2").start();
}
}
场景 3:条件变量(生产者-消费者模型)
通过 Condition 实现“当队列满时生产者等待,队列空时消费者等待”的精细化唤醒逻辑。
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* 基于 ReentrantLock + Condition 实现生产者-消费者模型
*/
public class ProducerConsumerWithLock {
private final Queue<String> queue = new LinkedList<>();
private final int MAX_CAPACITY = 5; // 队列最大容量
private final ReentrantLock lock = new ReentrantLock();
// 生产者条件:队列满时等待
private final Condition producerCondition = lock.newCondition();
// 消费者条件:队列空时等待
private final Condition consumerCondition = lock.newCondition();
// 生产者:生产数据放入队列
public void produce(String data) throws InterruptedException {
lock.lock();
try {
// 队列满时,生产者等待
while (queue.size() == MAX_CAPACITY) {
System.out.println("队列已满,生产者 " + Thread.currentThread().getName() + " 等待");
producerCondition.await(); // 释放锁,进入等待状态
}
// 生产数据
queue.offer(data);
System.out.println("生产者 " + Thread.currentThread().getName() + " 生产数据:" + data + ",队列大小:" + queue.size());
// 唤醒消费者(队列有数据了)
consumerCondition.signal();
} finally {
lock.unlock();
}
}
// 消费者:从队列获取数据消费
public String consume() throws InterruptedException {
lock.lock();
try {
// 队列空时,消费者等待
while (queue.isEmpty()) {
System.out.println("队列空,消费者 " + Thread.currentThread().getName() + " 等待");
consumerCondition.await(); // 释放锁,进入等待状态
}
// 消费数据
String data = queue.poll();
System.out.println("消费者 " + Thread.currentThread().getName() + " 消费数据:" + data + ",队列大小:" + queue.size());
// 唤醒生产者(队列有空闲位置了)
producerCondition.signal();
return data;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ProducerConsumerWithLock pc = new ProducerConsumerWithLock();
// 启动 2 个生产者线程
for (int i = 0; i < 2; i++) {
int producerId = i;
new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
pc.produce("数据-" + producerId + "-" + j);
Thread.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "生产者-" + i).start();
}
// 启动 3 个消费者线程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 7; j++) {
pc.consume();
Thread.sleep(300);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "消费者-" + i).start();
}
}
}
四、synchronized 与 Lock 的核心差异与选型建议
了解二者的特性后,核心是在实际场景中做出合理选择。下表梳理了二者的核心差异:
| 对比维度 | synchronized | Lock(ReentrantLock) |
|---|---|---|
| 锁的获取/释放 | 隐式自动完成 | 显式手动操作(lock()/unlock()) |
| 锁类型 | 仅非公平锁 | 支持公平锁/非公平锁(构造函数指定) |
| 中断响应 | 不支持(线程等待时无法被中断) | 支持(lockInterruptibly() 方法) |
| 超时获取 | 不支持 | 支持(tryLock() 带超时参数) |
| 条件变量 | 仅一个(wait()/notify()) | 多个(newCondition() 创建) |
| 锁状态查询 | 无法查询 | 支持(isLocked()、isHeldByCurrentThread() 等) |
| 性能 | JDK 1.6 后优化(锁升级),低并发性能优异,高并发与 Lock 接近 | 高并发场景下性能稳定,灵活度高但开销略大 |
| 使用复杂度 | 简单,无需关注锁释放 | 复杂,需手动释放锁,易出现锁泄漏 |
4.1 选型核心原则
-
优先使用 synchronized 的场景:
简单同步场景(如单个共享资源的增删改查),无需复杂锁操作; -
低并发或中并发场景,synchronized 的锁升级机制能保证良好性能;
-
团队对 Lock 机制不熟悉,希望降低开发成本与出错风险。
-
必须使用 Lock 的场景:
需要公平锁机制(如对业务顺序性要求高的场景); -
需要中断等待中的线程(如超时取消任务);
-
需要超时获取锁(避免死锁,提高系统可用性);
-
需要多个条件变量(如精细的生产者-消费者模型);
-
需要查询锁状态(如监控系统锁竞争情况)。
五、总结
synchronized 是 Java 内置的“安全牌”,简单高效,适用于大多数基础同步场景;Lock 是更灵活的“进阶工具”,通过显式操作满足复杂业务需求。二者并非互斥关系,而是互补关系。
在实际开发中,建议遵循“简单场景用 synchronized,复杂场景用 Lock”的原则。同时,无论选择哪种方案,都需注意:
-
缩小锁的范围(仅对临界区加锁),减少锁竞争;
-
避免锁嵌套(减少死锁风险);
-
结合业务特点选择锁类型(如高并发优先非公平锁)。
线程安全是高并发系统的基石,合理选择同步方案,才能在保证数据一致性的同时,最大化系统的并发性能。

457

被折叠的 条评论
为什么被折叠?



