乐观锁、悲观锁

一、乐观锁理论基础

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

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();
 }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值