乐观锁

转载 : https://xiaozhuanlan.com/topic/8459761302

乐观锁

1. 并发控制

在多线程环境下,为了保证线程安全,需要使用并发控制。

乐观锁

数据库管理系统中有事务的概念,它是一组操作,并且满足 ACID 特性。一个事务可以看成一组任务,而任务是由线程驱动的,因此事务也可以并发地执行。并发执行多个事务时,为了保证每个事务都具有 ACID 特性,也同样需要使用并发控制。

乐观锁

主要有两种并发控制方法:悲观并发控制和乐观并发控制,应该注意到它们都是思想,而不是具体的实现。

2. 悲观并发控制

悲观并发控制保持着悲观的态度,认为并发执行过程一定会出现问题,也就一定要做点什么去防止出现问题。

传统的锁机制都是悲观并发控制的实现,通过加锁从而保证多线程互斥地修改和访问共享数据,那么多线程就不会竞争使用共享数据,也就不会产生线程安全问题。

乐观锁

在下面的 Java 示例中,通过使用 synchronized 锁机制为 add() 和 get() 方法加锁,从而保证了多线程互斥地修改和访问 cnt 共享数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PressimisticLocking {



private int cnt = 0;



public synchronized void add() {

cnt++;

}



public synchronized int get() {

return cnt;

}

}

在数据库管理系统中提供了两种锁:共享锁(S)和排它锁(X),也称为读锁和写锁。

  • 一个事务对数据 A 加了 X 锁,就可以对 A 进行读取和更新,加锁期间其它事务不能对 A 加任何锁。
  • 一个事务对数据 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作,加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。

MySQL 的 InnoDB 存储引擎可以使用以下方法分别加 S 锁和 X 锁:

1
2
3
SELECT ... LOCK In SHARE MODE;

SELECT ... FOR UPDATE;

悲观并发控制能够保证线程安全性,但是无论共享数据是否真的会出现竞争,它都要进行加锁。而加锁操作需要涉及用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作,代价非常高。所以悲观并发控制只适合共享数据经常出现竞争的场景,但是对于共享数据基本不会出现竞争的场景,它花费了很多不必要的锁开销。

3. 乐观并发控制

针对悲观并发控制对于共享数据基本不会出现竞争的情况下也需要加锁的问题,出现了乐观并发控制。它保持乐观的态度,认为并发执行过程不会对共享数据出现竞争问题。它只在修改数据之后检测一下修改期间该共享数据有没有出现竞争问题,也就是说有没有其它线程也同样修改了该共享数据,如果没有则修改成功,如果有则需要重做。

乐观锁

CAS 实现

CAS 指令是硬件支持的操作:比较并交换(Compare-and-Swap),它需要有 3 个操作数,分别是内存地址 V、旧值 A 和新值 B。CAS 用于冲突检测,内存地址 V 中的值如果不等于旧值 A,则表示冲突,否则才将内存地址 V 的值更新为新值 B。

乐观锁

Java 中的 AtomicInteger 使用 CAS 来实现,以下使用 AtomicInteger 声明了一个共享变量 cnt,该共享变量在多线程环境下不会出现线程安全问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AtomicExample {



private AtomicInteger cnt = new AtomicInteger();



public void add() {

cnt.incrementAndGet();

}



public int get() {

return cnt.get();

}

}

AtomicInteger 的核心代码是 getAndAddInt(),其中 var1 是内存地址 V,getIntVolatile(var1, var2) 得到旧值 A,而 var5 + var4 是新值 B。可以看到在检测到冲突时,AtomicInteger 采用不断重试的方式,直到不再发生冲突为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
public final int getAndAddInt(Object var1, long var2, int var4) {

int var5;

do {

var5 = this.getIntVolatile(var1, var2);

} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;

}

如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过,这就是 ABA 问题。J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。

使用版本号实现

在数据库管理系统中,可以为表增加一个 version 列,那么每个记录就可以维护一个版本号,每次修改时版本号加 1。在对记录进行修改之前先读出 version,并在修改后判断 version 是否发生改变。如果没有发生改变就表示没有发生冲突,执行提交,否则回滚。

乐观锁

1
2
3
4
5
6
7
start transaction;

select version from t_goods where id=#{id};

update t_goods set status=2,version=version+1 where id=#{id} and version=version;

commit;