锁
锁
锁的访问机制
当然!“锁的访问机制”是一个涵盖多种策略和设计思想的广阔领域。它并非指某一种单一的技术,而是根据不同的需求场景,设计出的一系列解决并发问题的“规则”或“模式”。
我们可以将这些机制想象成是为了管理一个**“热门公共资源”(比如一个只能单人使用的卫生间)**而制定的不同规则。我们的目标是让大家(线程)能够安全、高效地使用这个资源。
下面我们来探讨几种最核心、最重要的锁访问机制:
1. 公平性机制:排队还是抢占?(Fairness)
当卫生间门口有多个人在等待时,谁应该下一个进入?
a) 公平锁 (Fair Lock)
- 规则:严格遵循先来后到。门口有一个排队队列,第一个来排队的人,在卫生间空出来后,第一个进入。
- 比喻:就像银行柜台的叫号系统,严格按照号码顺序办理业务。
- 优点:非常公平,所有等待的线程最终都有机会执行,不会出现“饿死”(Starvation)的情况。
- 缺点:吞吐量较低。因为需要严格维护一个队列,并且频繁地进行线程的挂起和唤醒。即使卫生间刚空出来,且恰好有一个新线程过来(队列中没有其他线程),这个新线程也必须先进入队列,再被系统从队列中取出并唤醒,这个过程有额外的开销。
- Java实现:
java.util.concurrent.locks.ReentrantLock可以在构造时指定为公平锁 (new ReentrantLock(true))。
b) 非公平锁 (Non-Fair Lock)
- 规则:谁抢到就是谁的。当卫生间空出来时,所有在门口等待的人(以及可能刚到的新人)都可以一拥而上尝试开门,谁手快抢到了就进去。
- 比喻:就像抢公交车的座位,门一开,谁离得近、跑得快谁就先上。
- 优点:吞吐量更高。因为它减少了线程切换的开销。一个线程释放锁后,可能立即再次获取锁,无需唤醒其他线程。或者一个新来的线程可能运气好,恰好在锁被释放的瞬间尝试获取,直接成功,避免了进入等待队列再被唤醒的成本。
- 缺点:可能会导致线程饿死。如果某个线程运气特别差,可能永远也抢不到锁。
- Java实现:
synchronized关键字实现的就是非公平锁。ReentrantLock默认也是非公平锁。
2. 共享性机制:独占还是共享?(Exclusivity)
这个公共资源一次能被多少人使用?
a) 独占锁 / 排他锁 (Exclusive Lock)
- 规则:卫生间一次只能进去一个人。无论这个人进去是读书、写字还是做其他事,只要门被锁上,其他任何人都不能进入。
- 适用场景:写操作。当一个线程需要修改共享数据时,必须独占资源,以防数据被其他线程破坏。
- Java实现:
synchronized和ReentrantLock都是独占锁。
b) 共享锁 (Shared Lock)
- 规则:这个资源是一个图书馆阅览室。
- 如果大家只是进去读书(读操作),那么可以多个人同时进入,互不影响。
- 但如果有人要进去整理书架(写操作),他必须等待阅览室里所有人都出来,然后他一个人进去并锁上门。在他整理期间,任何想读书的人都不能进入。
- 适用场景:读多写少的场景。允许多个线程同时读取共享资源,可以极大地提高并发性能。
- Java实现:
java.util.concurrent.locks.ReentrantReadWriteLock。它内部包含一个读锁(共享锁)和一个写锁(独占锁)。
3. 可重入性机制:能重复进入吗?(Reentrancy)
如果一个人已经在卫生间里了,他能否再次进入这个“已经被自己锁上”的卫生间?
a) 可重入锁 (Reentrant Lock)
- 规则:你用你的钥匙锁上了卫生间的门,进去了。在里面,你发现还有一个带锁的小柜子,而这个柜子用的恰好是同一把钥匙。你可以用手里的钥匙再次打开这个小柜子,而不会被自己锁在门外。
- 原理:锁内部会记录当前持有锁的线程,并维护一个计数器。当同一个线程再次请求锁时,计数器加一,然后直接成功。释放锁时,计数器减一,直到计数器为0,锁才被真正释放。
- 优点:避免了在递归调用或方法间相互调用时发生死锁。
public synchronized void methodA() {
// ... do something ...
methodB(); // 调用另一个同步方法
}
public synchronized void methodB() {
// ... do something else ...
}
如果synchronized不是可重入的,当methodA调用methodB时,线程会因为无法获取已经被自己持有的锁而永久等待,造成死锁。
- Java实现:
synchronized和ReentrantLock都是可重入锁。
b) 不可重入锁 (Non-Reentrant Lock)
- 规则:你用钥匙锁门进去了。当你尝试用同一把钥匙开里面的小柜子时,锁傻了,它只知道“锁已被占用”,于是让你在门外等待,结果造成了自己等自己的死锁。
- Java实现:在标准的JUC包中不直接提供,通常需要自己实现,实际应用场景较少。
4. 锁竞争策略:乐观还是悲观?(Optimism vs. Pessimism)
在尝试获取资源前,你的心态是怎样的?
a) 悲观锁 (Pessimistic Lock)
- 心态:“世界充满危险,总有人跟我抢!”
- 行为:在对数据进行任何操作之前,先将资源锁住。无论是否真的有其他线程在竞争,都先上锁再说。这是一种“先锁定,再操作”的模式。
- Java实现:
synchronized和所有Lock接口的实现类(如ReentrantLock)都是悲观锁的体现。
b) 乐观锁 (Optimistic Lock)
- 心态:“世界很美好,大概率没人跟我抢。”
- 行为:不加锁就直接去操作数据。在操作完成后,准备提交更新时,再去验证一下,在我操作期间,这个数据有没有被别人修改过。
- 如果没被修改,提交成功。
- 如果被修改了,说明发生了冲突。此时可以选择放弃、重试或报错。
- 实现方式:通常通过**版本号(Versioning)或CAS(Compare-And-Swap,比较并交换)**操作实现。
- Java实现:
java.util.concurrent.atomic包下的原子类,如AtomicInteger,其内部就广泛使用了CAS机制,这是乐观锁在Java中的典型应用。它适用于冲突较少的场景,可以避免传统锁的性能开销。
总结
| 机制分类 | 类型A | 类型B | 核心问题 |
|---|---|---|---|
| 公平性 | 公平锁 | 非公平锁 | 如何决定下一个谁来获取锁? |
| 共享性 | 独占锁 | 共享锁 | 一次允许多少个线程访问? |
| 可重入性 | 可重入锁 | 不可重入锁 | 已持有锁的线程能否再次获取该锁? |
| 竞争策略 | 悲观锁 | 乐观锁 | 如何看待并发冲突?是先加锁还是先操作? |
这些机制不是孤立的,一个具体的锁实现(如 ReentrantLock)可以是非公平的、独占的、可重入的悲观锁。理解这些机制,可以帮助你根据不同的并发场景,选择最合适的工具来编写高效且安全的代码。
更新: 2025-08-28 16:31:33
原文: https://www.yuque.com/duifangzhengzaishuru-rqbua/axyc58/pzd6r971bgzm211h