乐观锁和悲观锁详解
如果将悲观锁(Pessimistic Lock)和乐观锁(Optimistic Lock)对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。
悲观锁
悲观锁的核心思想是:假设最坏的情况,每次对共享资源的访问都会发生冲突,因此每次操作资源时都加锁,保证在同一时刻只有一个线程可以访问该资源,其他线程必须等待当前线程释放锁。换句话说,悲观锁总是采取最保守的策略,认为资源会被其他线程修改,因此通过加锁来防止数据一致性问题。
实现方式
synchronized
关键字synchronized
是 Java 中实现悲观锁的最常见方式。可以用于方法或代码块上,确保同一时刻只有一个线程可以访问被加锁的部分。方法级别的同步:对整个方法加锁,锁住当前对象实例(实例锁)或类(类锁)。
public synchronized void performTask() { // 需要同步的代码 }
代码块级别的同步:对代码块加锁,只对临界区代码加锁,控制粒度更小。可以指定锁的对象来控制不同的锁策略。
public void performTask() { synchronized(this) { // 需要同步的代码 } }
ReentrantLock
ReentrantLock
是 Java 中一种显式的锁,它提供了比synchronized
更灵活的锁控制方式,例如:可中断的锁、锁超时等特性。ReentrantLock
是可重入的,即同一线程可以多次获得该锁。Lock lock = new ReentrantLock(); lock.lock(); // 获取锁 try { // 需要同步的代码 } finally { lock.unlock(); // 释放锁 }
- 可中断性:
ReentrantLock
提供了lockInterruptibly()
方法,允许线程在等待锁时响应中断。 - 尝试获取锁:
tryLock()
方法允许线程尝试获取锁,而不是无限期地阻塞。
- 可中断性:
优缺点
优点:
- 数据一致性保障:通过加锁,保证了线程对共享资源的独占访问,确保数据一致性。
- 死锁控制:通过合理使用锁顺序,能够减少死锁的概率(但
synchronized
和ReentrantLock
本身并不能完全避免死锁)。
缺点:
- 线程阻塞:高并发情况下,线程竞争锁会导致大量线程阻塞,影响系统响应时间。
- 性能开销:线程阻塞和唤醒会引起上下文切换,增加系统负担。
- 死锁问题:如果锁获取顺序不当,可能会引发死锁。
适用场景
悲观锁适用于写操作较多、竞争较激烈的场景。在这种场景下,确保数据一致性至关重要,使用悲观锁可以避免数据的竞争条件和不一致问题。典型的应用包括数据库事务操作、多线程更新共享资源的场景等。
乐观锁
乐观锁的核心思想是:假设最好的情况,认为在多数情况下共享资源不会发生冲突,因此不加锁,允许多个线程并发操作资源。在提交修改时,才验证资源是否被其他线程修改。如果没有被修改,则更新数据;如果已经被修改,则重试或放弃操作。
乐观锁的关键在于冲突检测,当多个线程并发操作时,只会在数据提交前做冲突检查。常见的乐观锁实现方式是 CAS(Compare and Swap)算法和版本号机制。
实现方式
CAS(Compare and Swap)
CAS 是乐观锁最常用的实现机制,基于原子操作进行实现。它涉及到三个变量:- V:当前值(变量的当前值)
- E:期望值(当前线程读取的值)
- N:新值(当前线程要写入的新值)
当且仅当 V 等于 E 时,CAS 才会将 N 值写入 V,否则 CAS 会失败并返回 false。CAS 是一种无锁的原子操作,它依赖 CPU 的原子指令。
示例:
AtomicInteger counter = new AtomicInteger(0); counter.compareAndSet(0, 1); // 如果 counter 当前值为 0,则将其更新为 1
版本号机制
版本号机制在数据表中增加一个version
字段,表示数据的版本号。每次修改数据时,都会增加版本号。在提交修改时,线程会检查读取到的版本号与数据库中当前的版本号是否一致。如果一致,则更新数据并将版本号加一;如果不一致,说明数据已经被其他线程修改,当前线程需要重试。示例:
UPDATE account SET balance = balance - 50, version = version + 1 WHERE account_id = 123 AND version = 1;
优缺点
优点:
- 无锁并发:避免了线程阻塞和上下文切换,提高了并发性能。
- 死锁无忧:由于不涉及传统意义上的锁,所以不存在死锁问题。
- 性能较好:适合读多写少的场景,特别是需要高并发访问的环境。
缺点:
- 冲突检测的开销:频繁的重试机制可能导致性能下降,尤其在高写负载的情况下。
- ABA 问题:CAS 在处理数据修改时会遇到 ABA 问题,即在读取时数据没有变化,但在操作时却被改变过,导致错误的更新。可以通过版本号等方式解决。
适用场景
乐观锁适用于读多写少、竞争较少的场景。例如缓存系统、计数器、统计数据等。乐观锁特别适合于高并发且对数据一致性要求不那么严格的场景。
总结与选择
- 悲观锁:适用于写多读少的场景,通过加锁来防止数据冲突,保证数据一致性,但会引起线程阻塞和性能瓶颈。在高并发情况下,线程竞争严重时,悲观锁会导致线程阻塞和上下文切换,进而影响性能。
- 乐观锁:适用于读多写少的场景,线程无需阻塞,可以并发执行,只有在数据修改时才进行冲突检测。如果写操作较少,乐观锁能够有效提高系统吞吐量,减少锁竞争。
在选择时,悲观锁适合于数据冲突频繁且必须保证数据一致性的场景,如数据库操作;而乐观锁则适合于读操作频繁、写操作较少且对性能要求较高的场景,如缓存、计数器等。
最终选择的锁机制应根据具体的业务场景和性能需求进行权衡。