[面试]悲观锁与乐观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
[面试]悲观锁与乐观锁
乐观锁(Optimistic Lock)
基本概念
乐观锁是一种乐观思想:认为对于同一个数据的并发操作,是不会发生修改的。即认为读多写少,遇到并发写的可能性低
在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
即在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
只有在进行数据的提交更新时,才会检测数据的冲突情况,如果发现冲突了,则返回错误信息
使用场景
读多写少即冲突少(省锁开销,加大系统吞吐量)
实现方式
版本号机制
数据库中乐观锁的实现一般采用版本号
实现方式与原理
在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。
线程更新数据时,回读取数据和当前version值,提交数据时在此读取version值,只有两个version值相等时才可以提交数据。
核心SQL代码:
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};
CAS算法
Java中可使用CAS(Compare and Swap)实现乐观锁。是一种有名的无锁算法。
无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
CAS是由CPU硬件实现,所以执行相当快
实现方式与原理
涉及的操作数
需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
比较当前值跟传入值是否一样,一样则更新,否则失败。如果发生冲突失败重试,直到成功为止。
每个线程都可以访问,只有在提交数据的时候,检查是否违反了数据的完整性。
时间戳(timestamp)
给数据库表增加一个时间戳字段类型的字段,当读取数据时,将timestamp字段的值一同读出,数据每更新一次,timestamp也同步更新。当对数据做提交更新操作时,检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,若相等,则更新,否则认为是失效数据。
若出现更新冲突,则需要上层逻辑修改,启动重试机制
同样也可以使用version的方式。
缺点
1 ABA 问题–读脏数据的风险
由于基于比较A值的原理,只关注前后数据是否一致,忽略过程,所以当数据经过多次修改最终被改回初始A值得情景,会使得CAS操作认为没有数据修改
JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。
实例
java.util.concurrent包下面的原子类利用了CAS实现。
数据库提供的类似于write_condition机制
在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
悲观锁(Pessimistic Lock)
基本概念
悲观锁是就是悲观思想:认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,
因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
使用场景
写多读少
实现方式
悲观锁的实现方式主要通过维护一个内部锁池,当锁被占用时,从其他地方获取该锁都会失败。
依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
独占数据,其它线程需要等待,不会出现修改的冲突,能够保证数据的一致性
但是依赖数据库的实现,且在线程较多时出现等待造成效率降低的问题。
缺点
1、处理需要加锁和解锁,加锁解锁过程需要消耗资源。
2、当对某一部分进行加锁之后,其他线程只能阻塞,直到本线程处理完毕之后才有机会去竞争锁。
3、当低线程获得锁之后还在处理,优先级高的线程到达之后也需要阻塞等待,因此导致优先级高低线程倒挂处理问题。
实例
悲观锁对数据修改持保守态度,修改都要加排它锁,也常用于和数据库隔离级别结合使用
Synchronized:解决多线程并发问题,以保证事务的完整性。
传统的关系型数据库;行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
悲观锁和乐观锁使用场景
    1、对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
    2、对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
   补充: synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。