乐观锁、悲观锁

本文介绍了Java中的悲观锁和乐观锁。悲观锁拿数据时上锁,传统关系型数据库常用;乐观锁拿数据时不上锁,更新时判断。乐观锁适用于多读场景,悲观锁适用于多写场景。还介绍了乐观锁的实现方式、带来的ABA问题及解决方法。

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

两种锁的使用场景
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

乐观锁常见的两种实现方式
乐观锁一般会使用版本号机制或CAS算法实现。

1.版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

2.CAS算法
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数:

需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

乐观锁带来的问题:ABA

线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题,例如下面的例子:

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B;

在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态;

此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:

其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

解决方法

各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,其中compareAndSet方法会首先检查当前引用是否等于预期引用,其次检查当前标志是否等于预期标志,如果都相等就会以原子的方式将引用和标志都设置为新值。

例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABA {
        private static AtomicInteger atomicInt = new AtomicInteger(100);
        private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0);

        public static void main(String[] args) throws InterruptedException {
                Thread intT1 = new Thread(new Runnable() {
                        @Override
                        public void run() {
                                atomicInt.compareAndSet(100, 101);
                                atomicInt.compareAndSet(101, 100);
                        }
                });

                Thread intT2 = new Thread(new Runnable() {
                        @Override
                        public void run() {
                                try {
                                        TimeUnit.SECONDS.sleep(1);
                                } catch (InterruptedException e) {
                                }
                                boolean c3 = atomicInt.compareAndSet(100, 101);
                                System.out.println(c3); // true
                        }
                });

                intT1.start();
                intT2.start();
                intT1.join();
                intT2.join();

                Thread refT1 = new Thread(new Runnable() {
                        @Override
                        public void run() {
                                try {
                                        TimeUnit.SECONDS.sleep(1);
                                } catch (InterruptedException e) {
                                }
                                atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
                                atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
                        }
                });

                Thread refT2 = new Thread(new Runnable() {
                        @Override
                        public void run() {
                                int stamp = atomicStampedRef.getStamp();
                                try {
                                        TimeUnit.SECONDS.sleep(2);
                                } catch (InterruptedException e) {
                                }
                                boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
                                System.out.println(c3); // false
                        }
                });

                refT1.start();
                refT2.start();
        }
}

 

转载于:https://my.oschina.net/u/2286010/blog/3071735

<think>嗯,用户现在想了解乐观锁悲观锁的概念、使用场景,以及在Spring中的实现方式和区别。让我先回顾一下之前的对话和提供的引用内容,确保回答准确。 首先,用户之前问过Spring循环依赖的问题,我回答了三级缓存、解决流程和相关方案。现在用户转向了锁机制,特别是乐观锁悲观锁。需要结合提供的引用内容来构建答案。 根据引用[1],乐观锁悲观锁的基本概念已经提到,尤其是CAS算法和synchronized的优化。引用[2]到[5]详细说明了在Spring和JPA中的实现方式,包括使用版本号的乐观锁数据库锁机制的悲观锁,还有事务隔离级别的相关点。 用户的问题包括概念、使用场景、Spring实现方式和区别。需要分部分回答,结构要清晰。先解释两种锁的概念,然后对比它们的区别,接着在Spring中的具体实现方法,最后适用场景。同时,需要引用提供的资料中的例子,比如JPA的@Version注解和LockModeType的使用。 需要注意的是,用户可能对如何在Spring项目中实际应用这些锁机制感兴趣,所以代码示例会很有帮助。例如,引用[3]中的User实体类使用@Version,以及查询时加锁的方法。同时,引用[2]提到Spring Data JPA的EntityManager和JpaTemplate的使用,这也是实现悲观锁的方式。 另外,用户可能对事务隔离级别和锁的关系有疑问,比如引用[5]提到的事务隔离级别设置对锁的影响,需要简要说明。不过用户的主要问题集中在锁本身,所以这部分可能作为补充信息。 需要确保回答覆盖以下几点: 1. 定义和核心思想 2. 实现方式(包括Spring/JPA的具体方法) 3. 优缺点对比 4. 使用场景 5. 可能的问题和注意事项 还要注意引用标记,比如在提到JPA版本号时添加[^3],在数据库锁机制时引用[^2][^4]。同时,避免重复引用,保持引用标注正确。 现在,组织内容。先讲乐观锁,再悲观锁,然后对比,最后使用场景。每个部分包含概念、实现、代码示例(如果有)、优缺点。确保语言简洁,技术点明确,符合用户的中文提问需求。 检查是否有遗漏的信息,比如synchronized在Java中的演变,虽然用户的问题集中在Spring,但可能也需要简要提及,但根据引用[1],可以提到synchronized的优化,但重点还是在Spring的实现。 最后,生成相关问题,可能包括锁的选择建议、性能影响、在分布式系统中的处理等,但要根据引用内容中的点来设计,比如CAS的缺点、版本号冲突处理等。</think>--- ### 乐观锁悲观锁核心解析 #### **一、基本概念对比** | 特性 | 乐观锁 | 悲观锁 | |-------------------|----------------------------------------------------------------------|----------------------------------------------------------------------| | **核心思想** | 假设并发冲突概率低,仅在提交时检测冲突[^3] | 假设并发冲突概率高,操作前直接加锁[^4] | | **实现方式** | 版本号机制(如JPA的@Version)、CAS算法 | 数据库锁(SELECT FOR UPDATE)、Java同步锁(synchronized) | | **数据一致性** | 最终一致性 | 强一致性 | | **性能消耗** | 无锁设计,高并发下吞吐量高 | 锁竞争导致性能下降 | --- #### **二、Spring中的实现方式** ##### **1. 乐观锁实现** **JPA版本号机制**(推荐方案)[^3] ```java @Entity public class Product { @Id @GeneratedValue private Long id; private String name; private Integer stock; @Version // 核心注解,自动管理版本号 private Integer version; } ``` - **更新时自动校验**:若版本号不匹配,抛出`OptimisticLockException` - **适用场景**:电商库存、订单状态等低频冲突业务 ##### **2. 悲观锁实现** **方案一:数据库锁(Spring Data JPA)**[^2] ```java @Repository public class ProductRepository { @PersistenceContext private EntityManager em; public Product findWithLock(Long id) { return em.find(Product.class, id, LockModeType.PESSIMISTIC_WRITE); // 加写锁 } } ``` **方案二:Java同步锁** ```java @Service public class PaymentService { private final Object lock = new Object(); public void transfer() { synchronized(lock) { // 互斥访问 // 核心业务逻辑 } } } ``` --- #### **三、关键差异分析 1. **冲突处理方式** - 乐观锁:通过`重试机制`解决冲突(需业务层处理异常)[^3] - 悲观锁:通过`阻塞等待`避免冲突(由数据库/JVM保证) 2. **事务隔离级别关联** - 乐观锁通常配合`Read Committed`级别(平衡性能一致性)[^5] - 悲观锁需要`Repeatable Read`或`Serializable`级别支持 3. **典型异常类型** | 锁类型 | 异常类 | 触发条件 | |--------|-------------------------|----------------------| | 乐观锁 | OptimisticLockException | 版本号或CAS值不匹配 | | 悲观锁 | LockTimeoutException | 获取锁超时 | --- #### **四、选型决策树** ```mermaid flowchart TD A{是否需要强一致性?} A --> |是| B[选择悲观锁] A --> |否| C{冲突频率如何?} C --> |高| D[悲观锁+事务优化] C --> |低| E[乐观锁+重试机制] ``` --- #### **五、生产环境建议** 1. **混合使用场景** - 对核心资金操作使用`悲观锁+@Transactional` - 对商品库存使用`乐观锁+重试策略`[^4] 2. **性能优化技巧** ```java @Retryable(value = OptimisticLockException.class, maxAttempts = 3, backoff = @Backoff(delay = 100)) public void updateProduct(Product product) { // 带版本号的更新操作 } ``` (使用Spring Retry实现自动重试) 3. **分布式系统扩展** - 乐观锁:结合Redis实现分布式版本号 - 悲观锁:使用Redisson分布式锁 --- ### 相关问题 1. **CAS算法Java中如何实现?有什么缺陷?** 2. **Spring Retry如何配置自定义重试策略?** 3. **分布式场景下如何选择锁机制?** 4. **JPA的@Version字段能否使用其他数据类型?** [^1]: Java通过偏向锁、轻量级锁等优化提升synchronized性能 : Spring Data JPA提供原生悲观锁支持 [^3]: JPA通过@Version注解实现乐观锁 [^4]: 高并发场景建议根据业务特性选择锁机制 : 事务隔离级别设置影响锁机制的实际效果
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值