对CAS(乐观锁)的一些认识

“锁”是并发编程中再熟悉不过的了。当多个线程同时操作一个变量的时候,我们需要为这个变量加上一把锁,来保证同一时刻,只有一个线程在修改这个变量。那么,锁是如何工作的呢?

本文中,笔者主要谈谈乐观锁以及悲观锁。

先说说悲观锁。

笔者认为,悲观锁可以简单地概括为:简单粗暴。所谓悲观锁,也就是说,这个变量十分地担心自己同一时刻被多个变量修改,为了防止这种情况的发生,会使用一把锁,将这个变量牢牢地锁住。也就是说,当其中一个线程,获取到了这把锁,那么这个时候,其它的线程,就无法再获取到这把锁直到获取锁的线程,完成了对变量的修改之后释放掉这把锁。这种锁可以说是相当安全的,但是,也是相当低效的。当一个线程正在对共享变量进行操作的时候,其它的线程,就是挂起状态,也就是说只能干等着,啥也干不了。对于并发量极大的系统来说,这是完全不能接受的。

那么有什么办法,来解决这个问题呢?

此时就需要使用乐观锁。CAS,全称(Compare and Swap,比较并替换)。字面意思很明确了,先比较,再替换。那么,比较啥?又替换啥?

我们知道,每一个线程,都会有自己的一个工作空间,每次工作的时候,线程会把要修改的值先从主内存中复制一份出来,然后进行修改操作之后,再写回到主内存当中。那么此时,对于一个线程来讲,它的工作空间中的某个变量的值,跟主内存中对应的变量的值,如果是保持一致的,我们是否就可以认为,对于这个线程来说,这个共享变量没有被其它的线程修改过呢?CAS就是这么实现的。

对于一次CAS操作,有三个变量会被维护,它们是:期望值,老值以及新值。期望值,就是当前主内存中的共享变量的值;老值,就是当前线程工作空间中维护的值;新值,就是这个线程将要对主内存中的共享变量写入的值。

当该线程要对共享变量进行修改的时候,我们会先检查,当前线程的工作空间中的值和主内存中的值,是否一致,也就是上文中提到过的,如果保持一致,那么该线程就认为,没有其它的线程对这个变量进行过修改,它可以放心的对变量进行操作。那么,此时第一步就是,比较老值和期望值,是否相等。如果相等,那么此时该线程就可以安全地对共享内存进行修改。也就是,将新值,写入到主内存中。如果不相等,那么该线程就是获取锁失败,此时,该线程就会把自己工作空间中的老值,更新为当前的期望值。Figure1引用了维基百科上的一段代码,是CAS操作的一段C语言实现。

对CAS(乐观锁)的一些认识

Figure1

这段代码中,*reg是指向主内存中对应共享变量的一个指针,reg解指针之后的值,就是共享变量的值,也就是我们所说的期望值。接着比较,当前线程工作空间中的值和期望值是否相等。如果相等,那么该线程就可以对共享变量进行修改,也就是图中的*reg=newval。如果不是,则该线程获取乐观锁失败,此时会返回old_reg_val,也就是期望值。该线程要做的工作就是,将自己工作空间中的值,修改为期望值。

准确地说,乐观锁不是一种锁,而是一种无锁算法,CAS就是一种无锁算法。在实际应用中,除了CAS算法之外,还有一种做法是版本控制。常用的MySql使用的乐观锁便是使用了版本控制。每次对一个变量进行了一次修改之后,就会更新该共享变量的版本号。

CAS是一种CPU指令,调用CAS时,就是直接调用了该CPU指令,可以说是相当高效。然而,同样,CAS也存在的缺陷。我们知道,CAS校验的是主内存的值和线程自己工作空间的值。此时我们可以设想一种bad case,线程A的old value是1,线程B将内存值从1改成了2,线程C将内存值从2改成了1,这个时候,线程A试图去进行CAS,发现,期望值和老值保持一致,允许进行变量修改。这种case显然是不对的,这也就是乐观锁常说的ABA问题。当CAS获取锁失败时,线程就会进入

自旋状态,也就是我们常说的while true,在一个循环当中,不断尝试CAS操作来试图获取修改变量的资格。如果一直获取失败,那么将会给CPU带来不小的开销!

因此,对于乐观锁以及悲观锁,各有各的试用场景。写多读少的场景,使用悲观锁更加合适;相反,读多写少的场景,当使用乐观锁。


分享到:


相關文章: