乐观锁实现案例

发布于 2021-06-12  968 次阅读


一、悲观锁与乐观锁

锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发情况下的两种不同策略。

悲观锁(Pessimistic Lock), 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。

乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁!但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)。

悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

二、乐观锁的基础——CAS

说到乐观锁,就必须提到一个概念:CAS

什么是CAS呢?Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置

1、比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)。

2、设置:如果是,将A更新为B,结束。[1]如果不是,则什么都不做。

上面的两步操作是原子性的,可以简单地理解为瞬间完成,在CPU看来就是一步操作。

有了CAS,就可以实现一个乐观锁

data = 123; // 共享数据

/* 更新数据的线程会进行如下操作 */
flag = true;
while (flag) {
    oldValue = data; // 保存原始数据
    newValue = doSomething(oldValue); 

    // 下面的部分为CAS操作,尝试更新data的值
    if (data == oldValue) { // 比较
        data = newValue; // 设置
        flag = false; // 结束
    } else {
	// 啥也不干,循环重试
    }
}
/* 
   很明显,这样的代码根本不是原子性的,
   因为真正的CAS利用了CPU指令,
   这里只是为了展示执行流程,本意是一样的。
*/

这是一个简单直观的乐观锁实现,它允许多个线程同时读取(因为根本没有加锁操作),但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试。 CAS利用CPU指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。

Java中真正的CAS操作调用的native方法

因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!

案例:

无锁状态下多线程修改数据所带来的错误:

这个例子很简单:我们定义了一个变量datanum,初始值是0,然后使用2个线程去增加,每个线程增加20,按道理来说2个线程一共增加了40,但是运行一下就知道答案不到40,原因就在于里面那个加一操作:datanum++;

对于datanum++的操作,其实可以分解为3个步骤。

(1)从主存中读取datanum的值

(2)对datanum进行加1操作

(3)把datanum重新刷新到主存

比如说有的线程已经把datanum进行了加1操作,但是还没来得及重刷入到主存,其他的线程就重新读取了旧值。这才造成了错误

 

比如说有的线程已经把datanum进行了加1操作,但是还没来得及重刷入到主存,其他的线程就重新读取了旧值。这才造成了错误。解决办法就可以使用AtomicInteger:

现在我们使用AtomicInteger定义datanum,然后使用incrementAndGet进行自增操作,最后的结果就总是40了。

源码分析:incrementAndGet方法,这里我们可以看到自增操作主要是使用了unsafe的getAndAddInt方法

unsafe的getAndAddInt方法调用了CAS方法

CAS方法是Native方法,保证了操作的原子性

所以 我们使用AtomicInteger定义datanum,然后使用incrementAndGet进行自增操作,最后的结果就总是40了。

补充:

(1)Unsafe:Unsafe是位于sun.misc包下的一个类,Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力。也就是说我们直接操作了内存空间进行了加1操作。

(2) unsafe.getAndAddInt:其内部又调用了Unsafe.compareAndSwapInt方法。这个机制叫做CAS机制,

CAS 即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。

CAS会带来ABA,详解可搜索我的博客ABA问题

 

乐观锁的实现:

乐观锁可以由CAS机制+版本机制来实现。

实现测试:

 


她喜欢所以就做咯