浅谈CAS与乐观锁
CAS:Compare and Swap, 翻译成比较并与交换。
CAS的定义是这样的,看看就好,我也是百度复制的,懒得自己描述了。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
以上就是CAS定义。
看完CAS的定义后,就可以说乐观锁机制了,在软件开发之中,遇到并发问题时大量的场景使用到了乐观锁机制。
与乐观锁机制相对应的,即为悲观锁, 乐观和悲观这俩词体现在哪儿呢,既然知道一点:场景为并发,那么就可以得知乐观和悲观就体现在认为并发操作是否真的有存在。
首先,只要是并发修改的场景,为了保证并发安全,加锁机制是肯定需要的, 就看你用个啥解决方案。
若果说我认为并发场景时时有,每秒钟都有大量的线程同时请求。 那么可以说 我比较悲观。
如果说我觉得并发并不是那么多,可能半天才有几个并发,而且操作一般还很快速,那么可以说 我比较乐观。
既然得知了以上的悲观乐观概念,那么解决方案也就浮现而出了
悲观锁:我觉得你并发多,那我就直接用 阻塞/拒绝等 方式来控制你的请求,让数据只可能被一个线程同时操作。
乐观锁:我认为你没啥并发 干脆就不加锁了,你要修改啥玩意都直接改,不过为了保证并发安全,要有个判定机制。 一般来说那就是CAS。
悲观锁这玩意就不具体说了,就是为了控制临界区只能同时被一个线程访问设计出来的,遇到并发就线程阻塞/请求失败,效率低,并且加解锁这套操作指不定要调用什么消耗资源的命令,比如java中的 synchronized关键字(1.6以前) 就是一个标准的悲观锁,加解锁时还要使用操作系统的命令导致cpu用户态与内核态的切换,加解锁的时间指不定比你执行的临界区代码时间还多。
下面就主要说下乐观锁在哪些地方有用到
如果是数据库的并发场景:
这里我说的数据库并发场景只讨论”锁” 如何控制并发问题, 不讨论脏读幻读不可重复读这些由于事务隔离级别产生的并发不安全结果。
比如MySQL,MySQL为了并发安全自然里面内置了锁的机制,不过MySQL实现的锁机制是悲观锁(我这说的是写锁啊),增删改的时候默认就给你带上了。MyISAM存储引擎就是使用表锁,你增删改的时候一次锁你一张表,InnoDB存储引擎 使用的就是行锁,只锁你的操作数据行。
行锁表锁好处和坏处也很简单,因为加锁耗时间,所以MyISAM表锁开销小、加锁快,就是锁范围大了,容易发送锁冲突,并发度低。InnoDB行锁开销大、加锁慢。锁的范围小,不易发生锁冲突,并发度高。
而乐观锁这玩意 MySQL 是没给你实现的,不过乐观锁机制说简单也简单,无脑CAS就完事了,数据库的操作想使用乐观锁机制可以从表设计和代码逻辑上实现。
既然乐观锁机制的核心就是CAS操作,修改的值是可以确定好的,就是我执行SQL语句要改啥嘛,那么就差个所谓的内存位置和预期原值了。
内存位置和预期原值这俩词听起来挺抽象,其实就是一个更改的基准判断,在数据库场景中只要在表里加个字段就完事了,一般来说是version或者timestamp,以version版本举例的话那就是每次修改的时候都先查出来我要改的这个字段的版本号,然后在执行 SQL 语句进行修改,不过 SQL 中需要带一个 WHERE 子句,条件为 version = {查出来的版本},如果是 UPDATE 语句的话则同时将版本号+1
当当当,一个标准的CAS操作出现了,标准的比较交换流程,比较版本号,若和我之前查询出来的一致则交换,代表此时并没有人修改此字段。
若是我在修改前 此版本号已经被别人修改,那么由于此SQL带了判定的WHERE子句,直接将导致此SQL不成立。
一个乐观锁就这么实现了,而且数据库的这种场景还不用考虑ABA的问题,反正版本在每次修改时都不会一样。 然后在代码逻辑里根据SQL的返回值判定结果,成功就继续,失败要不触发失败操作,要不自旋抢锁。
如果是java程序当中的并发场景:
java当中 synchronized 关键字的锁膨胀阶段中 偏向锁、轻量级锁 都可以说是一个乐观锁(这里说的是经过1.6优化之后的)。
偏向锁 意思是偏向于第一个获取此锁的线程,获取锁的操作也是CAS操作,这里对java对象头不详细说,总之只要将java对象头中偏向线程ID以CAS操作改成自己的成功了,就代表获取到锁了。
如果存在并发情况,第二个线程试图获取锁的时候就会等所有线程到达全局安定点后将其升级为轻量级锁,这个轻量级锁就是一个典型的CAS+自旋操作 乐观锁。
轻量级锁情况下线程想要获取锁会直接在自己的线程栈中创建一个锁记录,然后将锁对象的对象头中markwork复制到自己的锁记录里面来,再试图将此对象对象头的markwork指针直接指向自己的锁记录。这套CAS流程要是失败,就自旋再次重复。经典的乐观锁机制。
CAS和乐观锁这个东西应该这篇文章算是解释清楚了,还举了几个例子比如数据库如何实现乐观锁、java中有哪些地方在使用乐观锁,synchronized在1.6之后的优化没仔细说,锁膨胀的过程只是简单地过了下,因为这个文章主要还是讲讲CAS和使用CAS实现乐观锁机制的,synchronized底层不在文章讨论范围之内,而且这玩意搜索引擎里边资料到处是,也没啥必要细说。
1 COMMENT