锁的访问机制

当然!“锁的访问机制”是一个涵盖多种策略和设计思想的广阔领域。它并非指某一种单一的技术,而是根据不同的需求场景,设计出的一系列解决并发问题的“规则”或“模式”。

我们可以将这些机制想象成是为了管理一个**“热门公共资源”(比如一个只能单人使用的卫生间)**而制定的不同规则。我们的目标是让大家(线程)能够安全、高效地使用这个资源。

下面我们来探讨几种最核心、最重要的锁访问机制:

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实现synchronizedReentrantLock 都是独占锁。

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实现synchronizedReentrantLock 都是可重入锁。

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