乐观锁、悲观锁

一、乐观锁理论基础

        所谓的乐观锁,其实主要就是一种思想,因为乐观锁的操作 过程中其实没有没有任何锁的参与,乐观锁只是和悲观锁相 对,严格的说乐观锁不能称之为锁。所以要了解乐观锁的概 念,通常与悲观锁对比起来看才更好理解,下面我们就通过 乐观锁与悲观锁的对比来更好的理解乐观锁。         

1.1乐观锁与悲观锁的概念

        乐观锁:总是假设最好的情况,每次去拿数据的时候都认为 别人不会修改,所以不会上锁,只在更新的时候会判断一下 在此期间别人有没有去更新这个数据。

        注意“在此期间”的含义是拿到数据到更新数据的这段时 间。因为没有加锁,所以别的线程可能会更改。还有一点那 就是乐观锁其实是不加锁的来保证某个变量一系列操作原子 性的一种方法。

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

1.2 两种锁的使用场景

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

1.3 乐观锁常见的两种实现方式

        乐观锁可以通过版本号机制或者CAS算法实现。

1.3.1 版本号机制

        版本号机制实现的方式常用的也有两种:

                使用数据版本(Version)记录机制实现,这是乐观锁最常 用的一种实现方式。何谓数据版本?即为数据增加一个版本 标识,一般是通过为数据库表增加一个数字类型的“version” 字段来实现。当读取数据时,将version字段的 值一同读出,数据每更新一次,对此version值加一。

        当我们提交更新的时候,判断数据库表对应记录 的当前版 本信息与第一次取出来的version值进行比对,如果数据库 表当前版本号与第一次取出来的version值相等,则予以更 新,否则认为是过期数 据。用下面的一张图来说明:

如上图所示,如果更新操作顺序执行,则数据的版本 (version)依次递增,不会产生冲突。但是如果发生 有不同的业务操作对同一版本的数据进行修 改,那 么,先提交的操作(图中B)会把数据version更新为 2,当A在B之后提交更新时发现数据的version已经被 修改了,那么A的更新操作会失败。 

使用时间戳(timestamp)。这种实现方式和第一种差 不多,同样是在需要乐观锁控制的table中增加一个字 段,名称无所谓,字段类型使用时间戳 (timestamp), 和上面的version类似,也是在更新提 交的时候检查当前数据库中数据的时间戳和自己更新前 取到的时间戳进行对比,如果一致则OK,否则就是版 本冲突。

1.3.2 CAS算法

        即Compare And Swap(比较与交换),是一种有名的无 锁算法。无锁编程,即不使用锁的情况下实现多线程之间的 变量同步,也就是在没有线程被阻塞的情况下实现变量的同 步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS操作包含三个操作数——内存位 置的值(V)、预期原值(A)和新值(B)。执行CAS操作 的时候,将内存位置的值与预期原值比较,如果相匹配,那 么处理器会自动将该位置值更新为新值,否则,处理器不做 任何操作。

        我们使用一个例子来解释相信你会更加的清楚。

        1.在内存地址V当中,存储着值为10的变量。

        2.此时线程1想要把变量的值增加1。对线程1来说,旧的预 期值A=10,要修改的新值B=11。

        3.在线程1要提交更新之前,另一个线程2抢先一步,把内存 地址V中的变量值率先更新成了11。

        4.线程1开始提交更新,首先进行A和地址V的实际值比较 (Compare),发现A不等于V的实际值,提交失败。

        5.线程1重新获取内存地址V的当前值,并重新计算想要修改 的新值。此时对线程1来说,A=11,B=12。这个重新尝试 的过程被称为自旋。

        6.这一次比较幸运,没有其他线程改变地址V的值。线程1进 行Compare,发现A和地址V的实际值是相等的。

        7.线程1进行SWAP,把地址V的值替换为B,也就是12。

        注:CAS算法的缺点

        【1】循环时间长开销很大:自旋 CAS 如果长时间不成功, 会给 CPU 带来非常大的执行开销。

        【2】只能保证一个共享变量的原子操作:只能保证一个共 享变量的原子操作。当对一个共享变量执行操作时,我们可 以使用循环 CAS 的方式来保证原子操作,但是对多个共享 变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变 量合并成一个共享变量来操作。比如有两个共享变量 i= 2,j=a,合并一下 ij=2a,然后用CAS 来操作 ij。从 Java1.5 开始 JDK 提供了 AtomicReference 类来保证引用对象之间 的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。

        【3】ABA 问题:因为 CAS 需要在操作值的时候检查下值 有没有发生变化,如果没有发生变化则更新,但是如果一个 值原来是A,变成了B,又变成了A,那么使用 CAS 进行检 查时会发现它的值没有发生变化,但是实际上却变化了。 ABA 问题的解决思路就是使用版本号。在变量前面追加上 版本号,每次变量更新的时候把版本号加1,那么A-B-A 就会变成1A-2B-3A。 使用

        version版本号完成乐观锁测试

package com.iweb.airui369.booktest;

public class Books {
     private String bookid;
     private String bookname;
     private int remain;
     private int version;

     public Books() {
     }

 public Books(String bookid, String bookname, int remain, int version){
     this.bookid = bookid;
     this.bookname = bookname;
     this.remain = remain;
     this.version = version;
 }

 public String getBookid() {
     return bookid;
 }

 public void setBookid(String bookid) {
     this.bookid = bookid;
 }

 public String getBookname() {
     return bookname;
 }

 public void setBookname(String bookname) {
     this.bookname = bookname;
 }

 public int getRemain() {
     return remain;
 }
    
 public void setRemain(int remain) {
     this.remain = remain;
 }

 public int getVersion() {
     return version;
 }

 public void setVersion(int version) {
     this.version = version;
 }

@Override
 public String toString() {
     return "Books{" +
         "bookid='" + bookid + '\'' +
         ", bookname='" + bookname + '\'' +
         ", remain=" + remain +
         ", version=" + version +
             '}';
 }
}
package com.iweb.airui369.booktest;

import com.iweb.airui369.emptest.dao.BaseDao;

public class BookThread implements Runnable {
     BaseDao baseDao = new BaseDao();
     @Override
     public void run() {
         while (true){
             String sql1 = "select * from tb_books where bookid = 1001";
             Books books = baseDao.QueryOne(Books.class, sql1);
             int version = books.getVersion(); //获取当前版本号
             System.out.println(Thread.currentThread().getName()+"拿到的版本
                 +version);
             int remain = books.getRemain();
             //更新
             String sql2 = "update tb_books set remain = remain-1," +
                     " version = version + 1" +
                     " where bookid = '1001' and version = ?";
             boolean result = baseDao.update(sql2, version);

             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             if (result) {
                 System.out.println(Thread.currentThread().getName() + "---更
                 return;
             }else {
                 System.out.println("前方网络拥堵,请稍后再试");
             }
         }
     }
}
public class test {
 public static void main(String[] args) {
 new Thread(new BookThread()).start();
 new Thread(new BookThread()).start();
 new Thread(new BookThread()).start();
 new Thread(new BookThread()).start();
 }
}

<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、付费专栏及课程。

余额充值