导图社区 乐观锁和悲观锁
根据作者:编程密思的博文【BAT面试题系列】面试官:你了解乐观锁和悲观锁吗?整理的知识点。
社区模板帮助中心,点此进入>>
互联网9大思维
组织架构-单商户商城webAPP 思维导图。
域控上线
python思维导图
css
CSS
计算机操作系统思维导图
计算机组成原理
IMX6UL(A7)
考试学情分析系统
乐观锁和悲观锁
基本概念
乐观锁
乐观锁在操作数据时非常乐观,认为别人不会同时修改数据
乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
悲观锁
悲观锁在操作数据时比较悲观,认为别人会同时修改数据。
操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
实现方式
CAS(Compare And Swap)
操作对象
1. 需要读写的内存位置(V)
这里的V是内存位置,我一直理解为在内存中的值
2. 进行比较的预期值(A)
3. 拟写入的新值(B)
操作步骤
如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。
许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。
原子性
CAS包含比较和交换两个动作,如何保存原子性?
CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。
乐观锁实例:java自增
AtomicInteger
java.util.concurrent.atomic
private static AtomicInteger value = new AtomicInteger(0);
value.getAndIncrement();
compareAndSet是CAS操作的核心,它是利用Unsafe对象实现的。
getAndIncrement()实现的自增操作是自旋CAS操作:在循环中进行compareAndSet,如果执行成功则退出,否则一直执行。
缺乏退出机制
AtomicInteger是java.util.concurrent.atomic包提供的原子类,利用CPU提供的CAS操作来保证原子性;
除了AtomicInteger外,还有AtomicBoolean、AtomicLong、AtomicReference等众多原子类。
版本号机制
版本号机制的乐观锁实现:基本思路是为数据增加一个版本号字段,读取时,也读取版本号字段,数据的版本号字段在每次写入时更新,作为数据的唯一性标识。
当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。
在更新时,通过where子句比对数据,一致时才能写入数据库。
update("update player set coins = {0} where player_id = {1} and version = {2}", newCoins, playerId, player.version);
代码块加锁(如:syncronized)
synchronized通过对代码块加锁来保证线程安全:在同一时刻,只能有一个线程可以执行代码块中的代码。
synchronized只是加锁,不代表就是重量锁
synchronized是一个重量级的操作,不仅是因为加锁需要消耗额外的资源,还因为线程状态的切换会涉及操作系统核心态和用户态的转换;不过随着JVM对锁进行的一系列优化(如自旋锁、轻量级锁、锁粗化等),synchronized的性能表现已经越来越好。
数据加锁(如:MySql的排它锁)
select coins, level from player where player_id = {0} for update
在查询信息时,使用select …… for update进行查询;该查询语句会为该数据加上排它锁,直到事务提交或回滚时才会释放排它锁;在此期间,如果其他线程试图更新该玩家信息或者执行select for update,会被阻塞。
优缺点
功能限制
主要指的乐观锁的限制问题。
例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。
再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。
是否指的读写分离库?
竞争激烈程度
当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势
当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。
面试问题
面试官追问:乐观锁加锁吗?
(1)乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了;AtomicInteger便是一个例子。
(2)有时乐观锁可能与加锁操作合作,例如,在前述updateCoins()的例子中,MySQL在执行update时会加排它锁。但这只是乐观锁与加锁操作合作的例子,不能改变“乐观锁本身不加锁”这一事实。
面试官追问:CAS有哪些缺点?
ABA问题
两个线程在执行时,若其中一个线程执行了一个更新A、更新B、再更新为A的操作,对于另外一个线程,其无法判断当前的是第一个A还是最后一个A。
可以结合版本号来一起解决问题:判别时,即比较值也比较版本号。
可能的危害:如栈顶值的维护,看起来栈顶似乎都是A,但实质上是不同的数据。
引入版本号,内存中的值每发生一次变化,版本号都+1;在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。
Java中的AtomicStampedReference类便是使用版本号来解决ABA问题的。
高竞争问题
在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。
当然,更重要的是避免在高竞争环境下使用乐观锁。
CAS只能保证单个变量(或者说单个内存值)操作的原子性
(1)原子性不一定能保证线程安全,例如在Java中需要与volatile配合来保证线程安全;
如何理解?
(2)当涉及到多个变量(内存值)时,CAS也无能为力。
CAS的实现需要硬件层面处理器的支持,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。