锁
乐观锁 VS 悲观锁
概念: 对并发操作数据时,看待线程同步的不同角度。
-
悲观锁:
- 假设: 并发修改频繁,数据冲突可能性高。
- 策略: 先加锁,再访问数据,确保数据操作的排他性。
- Java 实现:
synchronized
关键字和Lock
的实现类 (如ReentrantLock
)。 - 适用场景: 写操作多的场景,保证数据准确性.
-
乐观锁:
-
假设: 并发修改较少,数据冲突可能性低。
- 策略: 不加锁,直接访问数据,更新时检查数据是否被修改过。若未修改,则更新成功;若已修改,则根据策略处理 (报错或重试)。
- Java 实现: 无锁编程,常用 CAS (Compare-and-Swap) 算法。Java 原子类 (如
AtomicInteger
) 的递增操作基于 CAS 自旋实现. - 适用场景: 读操作多的场景,提高读取性能.
调用方式示例:
-
悲观锁:
```java // synchronized public synchronized void testMethod() { // 操作同步资源 }
// ReentrantLock private ReentrantLock lock = new ReentrantLock(); public void modifyPublicResources() { lock.lock(); try { // 操作同步资源 } finally { lock.unlock(); } } ```
-
乐观锁:
java private AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.incrementAndGet(); // CAS 自增操作
总结: 悲观锁显式加锁操作同步资源,乐观锁直接操作同步资源,依赖 CAS 机制保证同步.
自旋锁 VS 适应性自旋锁
前提知识: 线程阻塞/唤醒涉及操作系统 CPU 状态切换,开销较大.
-
自旋锁:
- 场景: 同步代码块执行时间短,锁占用时间短。
- 策略: 请求锁的线程 不放弃 CPU 时间片,而是 自旋等待 (循环检测锁是否释放)。若自旋成功,则避免线程切换开销.
- 缺点: 占用 CPU 时间。锁占用时间长时,自旋浪费 CPU 资源.
- 自旋次数限制: 默认 10 次,超过限制仍未获得锁,则挂起线程 (可使用
-XX:PreBlockSpin
修改). - 实现原理: CAS 算法。
AtomicInteger
的自增操作中的do-while
循环即为自旋操作. -
适应性自旋锁:
-
改进: 自旋次数 动态调整,根据 前一次自旋 在 同一个锁 上的 成功率 和 锁持有者 状态决定.
- 优势: 更智能的自旋策略,减少无效自旋,提高性能.
总结: 自旋锁适用于锁占用时间短的场景,通过自旋避免线程切换开销。适应性自旋锁更智能,动态调整自旋次数.
无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
锁状态: synchronized
锁升级状态,针对 synchronized
关键字.
-
无锁: 无锁状态。多个线程竞争资源时,都尝试通过 CAS 修改共享变量,但只有一个线程能成功,失败的线程会重试.
-
偏向锁:
- 场景: 单线程访问 同步代码块.
- 策略: 消除 线程获取锁的 CAS 操作。当线程首次获取锁后,将 偏向模式 和 线程 ID 记录在 对象头 Mark Word 中。后续该线程再次访问同步代码块时,只需检查 Mark Word 是否为偏向模式且线程 ID 是否为当前线程 ID,若是,则直接获得锁,无需 CAS 操作.
- 偏向锁撤销: 当有 其他线程竞争 偏向锁时,偏向模式结束,升级为轻量级锁.
-
轻量级锁:
-
场景: 多线程交替 访问同步代码块 (竞争不激烈).
- 策略: 使用 CAS + 自旋 尝试获取锁。线程在 栈帧 中创建 Lock Record (锁记录),通过 CAS 将 对象头 Mark Word 替换为指向 Lock Record 的指针。若替换成功,获得锁;若替换失败,自旋等待锁释放.
- 轻量级锁膨胀: 自旋超过一定次数或自旋线程数超过 CPU 核心数,轻量级锁膨胀为重量级锁.
-
重量级锁:
-
场景: 多线程并发高,锁竞争激烈.
- 策略: 线程获取锁失败后,阻塞 进入 等待队列,等待操作系统 唤醒。
synchronized
默认升级为重量级锁. - 缺点: 线程阻塞/唤醒开销大,性能较低.
总结: 锁状态升级顺序:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁. 偏向锁和轻量级锁旨在减少锁开销,提高性能,重量级锁是最后的保障.
公平锁 VS 非公平锁
概念: 多线程获取锁的顺序策略.
-
公平锁:
- 策略: 按申请锁的顺序 获取锁。线程进入 等待队列 排队,队列头部线程 优先获得锁.
- 优点: 等待线程不会饿死,公平性好.
- 缺点: 吞吐效率相对较低,队列中其他线程阻塞,CPU 唤醒开销较大.
- 实现:
ReentrantLock
可设置为公平锁 (构造函数传入true
). -
非公平锁:
-
策略: 线程尝试获取锁时,直接竞争。若获取不到,则进入等待队列队尾排队。若此时锁刚好释放,插队线程可能直接获取锁,无需阻塞.
- 优点: 吞吐效率高,减少唤醒开销,线程有机会不阻塞直接获得锁.
- 缺点: 等待队列中的线程可能饿死或等待时间过长,公平性差.
- 实现:
synchronized
和ReentrantLock
默认都是非公平锁.
可重入锁 VS 非可重入锁
概念: 同一个线程 重复 获取 同一个锁 的能力.
-
可重入锁 (递归锁):
- 特性: 线程在外层方法获取锁后,进入内层方法 自动 获取 同一把锁,不会因为已持有锁而阻塞.
- 优点: 避免死锁 (一定程度上).
- Java 实现:
synchronized
和ReentrantLock
都是可重入锁. -
非可重入锁:
-
特性: 线程重复获取锁时,会被 自己之前持有的锁 阻塞,导致死锁.
代码示例 (可重入锁):
public class Widget {
public synchronized void doSomething() {
System.out.println("方法1执行...");
doOthers(); // 调用另一个 synchronized 方法
}
public synchronized void doOthers() {
System.out.println("方法2执行...");
}
}
原理分析:
- AQS (AbstractQueuedSynchronizer):
ReentrantLock
和NonReentrantLock
都继承自 AQS. AQS 维护一个同步状态status
计数重入次数 (初始值为 0). - 可重入锁: 获取锁时,若
status == 0
,则设置status = 1
;若status != 0
且当前线程已持有锁,则status++
(重入计数增加). 释放锁时,status--
,当status == 0
时,真正释放锁. - 非可重入锁: 获取锁时,直接尝试更新
status
,若status != 0
,则获取锁失败,线程阻塞. 释放锁时,直接设置status = 0
.
独享锁 (排他锁) VS 共享锁
概念: 锁对资源的访问模式.
-
独享锁 (排他锁):
- 特性: 一次只能被一个线程持有. 线程 T 对数据 A 加排他锁后,其他线程 不能 对 A 加 任何类型的锁.
- 功能: 获得排他锁的线程既能 读 数据,又能 修改 数据.
- Java 实现:
synchronized
和 JUC 中Lock
的实现类 (如ReentrantLock
). -
共享锁:
-
特性: 可被多个线程同时持有. 线程 T 对数据 A 加共享锁后,其他线程 只能 对 A 加 共享锁,不能 加 排他锁.
- 功能: 获得共享锁的线程 只能读 数据,不能修改 数据.
- Java 实现:
ReentrantReadWriteLock
的 读锁 (ReadLock).
ReentrantReadWriteLock 示例:
- 读写锁分离:
ReentrantReadWriteLock
包含 读锁 (ReadLock) 和 写锁 (WriteLock). - 读锁: 共享锁,允许多个线程同时读取数据,并发读性能高.
- 写锁: 独享锁,保证 读-写互斥、 写-读互斥、 写-写互斥,保证数据一致性.
- 实现: 读锁和写锁都基于
Sync
(AQS 子类) 实现,但加锁方式不同.