作者:编程迷思

译文: http://cnblogs.com/kismetv/p/10787228.html

前言

乐观锁定和悲观锁定的问题是出现频率较高的问题。 希望本文慢慢介绍其基本概念、实现方法(包括实例)、适用场景以及可能遇到的面试官提问,有助于感动面试官。

目录

一、基本概念

二、实现方式(包括实例)

三、优缺点和适用场景

四、面试官问:“乐观上锁吗?

五、面试官问:“CAS有什么缺点?

六、总结

一、基本概念

乐观锁定和悲观锁定是两种思想,用于解决同时场景下的数据竞争问题。

乐观锁定:乐观锁定在操作数据时非常乐观,认为别人不会同时修改数据。 因此,乐观锁定不会被锁定,只需在执行更新期间判断其他人是否修改了数据即可。 如果其他人修改了数据,则放弃操作,否则执行操作。

悲观锁定:悲观锁定在数据操作时很悲观,认为别人会同时修改数据。 因此,操作数据时直接锁定数据,在操作完成之前不解除锁定; 上锁时其他人无法修改数据。

二、实现方式(包括实例)

在说明实现方法之前,需要明确的是,乐观锁定和悲观锁定是两种思想,它们的使用非常广泛,并不限于某个编程语言或数据库

悲观锁定既可以通过像Java同步关键字那样锁定代码块,也可以通过像MySQL独占锁定那样锁定数据来实现。

乐观锁定的实现方式主要有CAS机制和版本号机制两种,下面详细介绍。

1、cas (比较交换)。

CAS操作包含三个操作数。

需要读写的存储器位置(v )比较的预想值) a )写入的新值) b ) CAS操作逻辑如下。 如果存储器位置v的值等于预想a的值,则将该位置更新为新的值b,否则不做任何动作。 很多CAS的操作都是旋转的。 如果操作不成功,则重试直到操作成功。

在这里提出新的问题。 CAS包括Compare和Swap两个操作,那么如何保证原子性呢? 答案是,CAS是CPU支持的原子操作,其原子性在硬件层面得到了保证。

以Java中的自我增加操作(I )为例,来看看悲观锁定和CAS分别是如何确保线程安全的。 我们知道,Java中的自增操作不是原子操作,实际上包含三个独立的操作。

读取I的值; 加上1; 由于要将新值写回I,因此同时执行自我增加操作可能会导致计算结果不准确。 在下面的代码示例中,值1不保护线程安全,值2使用乐观锁定(CAS ),值3使用悲观锁定)同步。

如果运行程序,并使用1000个线程同时递增值1、值2和值3,则可以看到值2和值3始终为1000,值1始终小于1000。

数据库乐观锁(如何实现乐观锁)-冯金伟博客园

首先介绍原子集成器。 原子集成器是由java.util.concurrent.atomic软件包提供的原子类,利用CPU提供的CAS操作保证原子性; 除了原子集成器以外,还有原子布尔、原子长、原子参考等多种原子类。

下面,我们观察一下原子集成器的源代码,调查其自我增殖操作getAndIncrement () )是如何实现的。 源代码以Java7为例,Java8不同,但思想相似。

数据库乐观锁(如何实现乐观锁)-冯金伟博客园

源代码分析如下

1.getAndIncrement ()实现的自我增殖操作是自旋CAS操作。 在循环中进行比较,如果运行成功则结束,否则一直运行。

2 .其中compareAndSet是CAS操作的核心,它是利用Unsafe对象实现的。

3.Unsafe又是什么样的人? Unsafe是一个类,允许Java访问操作系统的底层资源,如分配内存、释放内存等,通过Unsafe,Java具有底层操作能力,从而提高执行效率。 由于强大的底层资源操作能力也带来了安全隐患,因此类名Unsafe也警告了这一点),用户通常不可用。 原子集成器在这里使用了Unsafe提供的CAS功能。

4.valueOffset可以理解为value内存中的偏移,与CAS的3个操作数(V/A/B )的v相对应。 偏移量的获取也是通过Unsafe实现的。

5.value域的volatile修饰符: Java并发编程为了确保线程的安全,需要保证原子性、可视性和秩序性; CAS操作可以保证原子性,volatile可以保证可视性和一定程度的秩序性; 在原子集成器中,为volati

le和CAS一起保证了线程安全性。关于volatile作用原理的说明涉及到Java内存模型(JMM),这里不详细展开。

说完了AtomicInteger,再说synchronized。synchronized通过对代码块加锁来保证线程安全:在同一时刻,只能有一个线程可以执行代码块中的代码。synchronized是一个重量级的操作,不仅是因为加锁需要消耗额外的资源,还因为线程状态的切换会涉及操作系统核心态和用户态的转换;不过随着JVM对锁进行的一系列优化(如自旋锁、轻量级锁、锁粗化等),synchronized的性能表现已经越来越好。

2、版本号机制

除了CAS,版本号机制也可以用来实现乐观锁。版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。

需要注意的是,这里使用了版本号作为判断数据变化的标记,实际上可以根据实际情况选用其他能够标记数据版本的字段,如时间戳等。

下面以“更新玩家金币数”为例(数据库为MySQL,其他数据库同理),看看悲观锁和版本号机制是如何应对并发问题的。

考虑这样一种场景:游戏系统需要更新玩家的金币数,更新后的金币数依赖于当前状态(如金币数、等级等),因此更新前需要先查询玩家当前状态。

下面的实现方式,没有进行任何线程安全方面的保护。如果有其他线程在query和update之间更新了玩家的信息,会导致玩家金币数的不准确。

数据库乐观锁(如何实现乐观锁)-冯金伟博客园

为了避免这个问题,悲观锁通过加锁解决这个问题,代码如下所示。在查询玩家信息时,使用select …… for update进行查询;该查询语句会为该玩家数据加上排它锁,直到事务提交或回滚时才会释放排它锁;在此期间,如果其他线程试图更新该玩家信息或者执行select for update,会被阻塞。

数据库乐观锁(如何实现乐观锁)-冯金伟博客园

版本号机制则是另一种思路,它为玩家信息增加一个字段:version。在初次查询玩家信息时,同时查询出version信息;在执行update操作时,校验version是否发生了变化,如果version变化,则不进行更新。

数据库乐观锁(如何实现乐观锁)-冯金伟博客园

三、优缺点和适用场景

乐观锁和悲观锁并没有优劣之分,它们有各自适合的场景;下面从两个方面进行说明。

1、功能限制

与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。

例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。

2、竞争激烈程度

如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:

当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。

当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

四、面试官追问:乐观锁加锁吗?

笔者在面试时,曾遇到面试官如此追问。下面是我对这个问题的理解:

1.乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了;AtomicInteger便是一个例子。

2.有时乐观锁可能与加锁操作合作,例如,在前述updateCoins()的例子中,MySQL在执行update时会加排它锁。但这只是乐观锁与加锁操作合作的例子,不能改变“乐观锁本身不加锁”这一事实。

五、面试官追问:CAS有哪些缺点?

面试到这里,面试官可能已经中意你了。不过面试官准备对你发起最后的进攻:你知道CAS这种实现方式有什么缺点吗?

下面是CAS一些不那么完美的地方:

1、ABA问题

假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:

(1)线程1读取内存中数据为A;(2)线程2将该数据修改为B;(3)线程2将该数据修改为A;(4)线程1对数据进行CAS操作

在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了。这就是ABA问题。

在AtomicInteger的例子中,ABA似乎没有什么危害。但是在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化。

对于ABA问题,比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1;在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。Java中的AtomicStampedReference类便是使用版本号来解决ABA问题的。

2、高竞争下的开销问题

在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。当然,更重要的是避免在高竞争环境下使用乐观锁。

3、功能限制

CAS的功能是比较受限的,例如CAS只能保证单个变量(或者说单个内存值)操作的原子性,这意味着:(1)原子性不一定能保证线程安全,例如在Java中需要与volatile配合来保证线程安全;(2)当涉及到多个变量(内存值)时,CAS也无能为力。

除此之外,CAS的实现需要硬件层面处理器的支持,在Java中发嗲的镜子无法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。

六、总结

本文介绍了乐观锁和悲观锁的基本概念、实现方式(含实例)、适用场景,以及可能遇到的面试官追问,希望能够对你面试有帮助。最后,祝大家都拿到心仪的offer!

参考文献

https://www.cnblogs.com/qjjazry/p/6581568.html
https://segmentfault.com/a/1190000016611415
https://www.cnblogs.com/pkufork/p/java_unsafe.html
https://stackoverflow.com/questions/19660737/aba-in-lock-free-algorithms
https://www.zhihu.com/question/23281499