一、乐观锁理论基础
所谓的乐观锁,其实主要就是一种思想,因为乐观锁的操作 过程中其实没有没有任何锁的参与,乐观锁只是和悲观锁相 对,严格的说乐观锁不能称之为锁。所以要了解乐观锁的概 念,通常与悲观锁对比起来看才更好理解,下面我们就通过 乐观锁与悲观锁的对比来更好的理解乐观锁。
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();
}
}