一、基本概念
乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。
- 乐观锁:操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据。如果别人修改了数据则放弃操作,否则执行操作。【在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。】
- 悲观锁:操作数据时比较悲观,认为别人会同时修改数据。因此修改数据时直接把数据锁住,知道操作完成以后才会释放锁,上锁期间其他人不能修改数据。【传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。】
二、实现方式
悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(synchronized关键字、ReentrantLock),也可以是对数据加锁(MySQL中的排它锁)。
乐观锁的实现方式主要两种:CAS和版本号控制
1.CAS(Compare and Swap:比较再交换),是一个多线程同步的原子指令,CAS包含三个重要信息:内存位置、预期原值、新值。如果内存位置的值和预期的原值相等的话,那么就可以把该位置的值更新为新值,否则不做任何修改。
2.数据版本:即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库增加一个version字段来实现。读取数据时,将此版本号一同读出,之后更新时,对此版本加1.同时将提交数据库的版本数据与数据库表对应的记录的当前版本信息进行对比,若二者相等,则予以更新,否则认为是过期数据。
三、优点缺点
乐观锁(Optimistic Locking)
优点:
1. 不会对数据进行加锁,提升并发性能。
2. 减少了数据库的开销,适合处理读操作多于写操作的场景。
3. 出现冲突时,处理起来较为简单,可以通过版本号等机制进行冲突检测和处理。
缺点:
1. 冲突发生的概率较高,如果频繁出现冲突,可能会影响系统性能。
2. 处理冲突时需要额外的逻辑,开发复杂性增加。
3. 当有大量并发操作时,可能需要多次尝试才能成功完成操作。
悲观锁(Pessimistic Locking)
优点:
1. 在操作数据时会直接对数据进行锁定,避免了数据被其他操作修改的风险。
2. 适合处理写操作多于读操作的场景,可以保证数据的一致性。
缺点:
1. 加锁会降低系统的并发性能,影响系统吞吐量。
2. 容易引发死锁问题,需要谨慎处理。
3. 长时间的锁定可能会阻塞其他操作,造成性能瓶颈。
综合考虑,选择乐观锁还是悲观锁取决于具体的业务场景和需求。在大多数情况下,乐观锁更适合处理高并发场景,而悲观锁适合需要保证数据一致性的场景。
(1) 悲观锁适合用于并发写入多、临界区代码复杂、竞争激烈等场景,这种场景下悲观锁可以避免大量的无用的反复尝试等消耗。
(2) 乐观锁适用于大部分是读取,少部分是修改的场景,也适合虽然读写都很多,但是并发并不激烈的场景。在这些场景下,乐观锁不加锁的特点能让性能大幅提高。
四、灵魂追问
1.乐观锁加锁吗?
答:乐观锁本身不加锁,只是在更新时判断一下数据是否被其他线程更新了;有时乐观锁可能与加锁合作,比如执行update时会加排它锁。但这只是与加锁合作。
2.CAS有哪些缺点?
答:【会出现ABA问题:指的是,线程拿到了最初的预期原值A,然而在将要进行CAS的时候,被其他线程抢占了执行权,把此值从A变成了B,然后其他线程又把此值从B变成A,然而此时的 A 值已经并非原来的 A 值了,但最初的线程并不知道这个情况,在它进行 CAS 的时候,就会误认为它从来没有被修改过,只对比了预期原值为 A 就进行了修改,这就造成了 ABA 的问题。】
- JDK在1.5时提供了AtomicStampedReference类也可以解决ABA的问题,此类维护了一个“版本号”Stamp,每次在比较时不止比较当前值还比较版本号,这样就解决了 ABA 的问题。
- 也可以采用CAS的一个变种DCAS来解决这个问题。DCAS,是对于每一个V增加一个引用的表示修改次数的标记符。对于每个V,如果引用修改了一次,这个计数器就加1。然后再这个变量需要update的时候,就同时检查变量的值和计数器的值。