锁是如何实现数据同步的?
锁通过 互斥性(Mutual Exclusion) 实现数据同步,确保同一时刻只有一个线程或进程能访问共享资源,防止并发操作导致的数据不一致。它将并行操作串行化,通过加锁和解锁机制控制访问权限,保障数据的正确性。
关键事实
- 目的:
- 解决竞争条件(Race Condition),如多线程同时修改变量。
- 原理:
- 锁被占用时,其他线程等待,直到锁释放。
- 实现:
- 依赖原子操作(如 CAS)或硬件支持(如
test-and-set
)。 - 类型:
- 互斥锁(Mutex)、读写锁、分布式锁等。
实现原理
1. 基本流程
- 加锁(Lock):
- 线程尝试获取锁,若成功则独占资源,若失败则等待。
- 访问共享资源:
- 执行临界区代码,操作数据。
- 解锁(Unlock):
- 释放锁,其他线程可竞争获取。
2. 底层机制
- 原子性:
- 加锁操作不可中断,基于硬件指令(如
cmpxchg
)。 - 示例:Java
synchronized
用monitorenter
指令。 - 状态标记:
- 锁维护状态(如 0=未锁,1=已锁)。
- 示例:CAS 修改锁状态。
- 等待队列:
- 未获取锁的线程阻塞或自旋,等待唤醒。
- 示例:AQS 的 CLH 队列。
3. 示例(Java)
class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) { // 加锁
count++; // 临界区
} // 解锁
}
}
- 无锁:多线程
count++
可能丢失更新。 - 加锁:每次只一个线程修改,
count
正确递增。
数据同步的具体实现
1. 互斥锁(Mutex)
- 原理:只有一个线程持有锁,其他线程阻塞。
- 场景:保护共享变量。
- Java:
synchronized
或ReentrantLock
。
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
2. 读写锁
- 原理:读共享,写独占,允许多线程同时读。
- 场景:读多写少。
- Java:
ReentrantReadWriteLock
。
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock(); // 读锁
rwLock.writeLock().lock(); // 写锁
3. 分布式锁(Redis)
- 原理:用 Redis 键值对标记锁状态,
SETNX
保证互斥。 - 场景:跨进程同步。
Jedis jedis = new Jedis();
if ("OK".equals(jedis.set("lock", "1", "NX", "PX", 30000))) {
// 获取锁,操作数据
jedis.del("lock"); // 释放锁
}
如何保证同步
- 互斥性:
- 锁确保单线程访问,防止并发冲突。
- 可见性:
- 锁释放后,内存屏障(Memory Barrier)刷新数据到主存。
- 示例:
synchronized
保证变量更新对后续线程可见。 - 原子性:
- 临界区操作不可分割,避免中间状态。
延伸与面试角度
- 性能影响:
- 锁减少并发度,高竞争下可能瓶颈。
- 优化:细粒度锁、读写分离。
- 与无锁对比:
- CAS:无锁但适用简单场景。
- 锁:阻塞式,适合复杂逻辑。
- 死锁问题:
- 多锁嵌套可能循环等待。
- 解决:资源排序、超时。
- 面试点:
- 问“原理”时,提互斥和原子性。
- 问“实现”时,提 CAS 或 AQS。
总结
锁通过互斥性串行化访问,用原子操作和等待机制实现同步。Java 用 synchronized
或 AQS,分布式用 Redis 等。面试时,可写 ReentrantLock
示例或提内存屏障,展示理解深度。