乐观锁与悲观锁的比较与应用场景

乐观锁和悲观锁的思想?

乐观锁就是认为每次线程对数据的访问并不存在冲突,没有别的线程去修改这个数据,因此并不会通过锁来控制线程之间对数据的竞争问题,而是在最终更新数据时确认一下数据没有被更新。

悲观锁是认为每次对数据的访问都存在冲突,每次都同时有多个线程在修改数据,因此当有一个线程对数据访问时必须通过上锁来阻塞其他线程对数据的访问。

两种方式的使用场景

根据两种锁的思想分析来看,一般认为乐观锁比较适用于冲突较少的场景,也就是读多写少的情况,对于数据的访问多数是读的访问,这样就可以通过乐观锁的方式省去了加锁、解锁的开销。

悲观锁就比较适用于冲突较多的场景,因为如果冲突较多的情况下再使用乐观锁则会造成大量的重试,不但加重了cpu的负担,也降低了整个系统的吞吐量,不如直接使用悲观锁。

两种锁的实现方式

在java中synchronized和lock就是悲观锁的方式,数据库的for update也是悲观锁。

乐观锁一般可以通过CAS或者版本号来实现。

1、版本号实现乐观锁
一般就是在表中新增一个版本号的字段,当需要更新数据时,先取出当前的版本号,然后再根据版本号来进行更新,如果更新失败就重新获取版本号,再尝试更新,依次循环直到更新成功。

比如有一条数据当前版本号version为1,此时有两个线程同时都获取到version值为1。

那么第一个线程执行更新时的语句就是
update table set name = xxx, version = version+1 where id = xxx and version = 1;
执行成功version更新为2,那么第二个线程再执行时就会更新失败,然后再次尝试重新获取version,此时得到的值为2,然后执行更新语句
update table set name = xxx, version = version+1 where id = xxx and version = 2;
更新成功,version的值最后为3。

2、基于CAS的方式
compare and swap(比较并交换),CAS主要有三个参数,
V:内存值
A:当前时
B:待更新的值
当且仅当V等于A时,就将A更新为B,否则什么都不做。V和A的比较是一个原子性操作保证线程安全。

乐观锁有什么缺点?

1、ABA问题
如果V等于A时,就将A更新为B,这在大多数场景下是没有问题的,但是如果V等于A时就一定代表A没有被更新过吗?答案不是的,因为有可能A已经被更新过其他值了,只不过之后又被更新回A了,这对当前要将值更新为B的线程来说是不知情的,这就是ABA问题。

如何解决ABA问题?
可以通过版本号来解决ABA问题,更新时同时比较版本号和值,java中的 AtomicStampedReference 类就提供了此种能力,compareAndSet 方法就是新增了检查当前引用是否等于预期引用。

2、循环比较消耗大
因为如果CAS更新失败一般就会通过自旋的方式重新尝试更新直到成功,那么如果经常失败此时CAS就会加重CPU的负担,也其实是乐观锁的缺点。

3、只能保证一个变量的安全
一般来说CAS只能保证一个变量的原子性操作,如果业务需求有多个共享变量的竞争,此时CAS就是适用了,但是jdk提供了AtomicReference,通过把多个变量封装成一个对象来进行CAS操作保证原子性。

简单的来说悲观锁就是适用于线程之间冲突较多的场景,而乐观锁适用于冲突较少的场景,我们说synchronized是属于悲观锁的应用,但是现在JDK的版本中synchronized已经经过优化,即使在冲突较少的场景性能其实也不比CAS差,而冲突较多的场景性能则要高于CAS。关于jdk1.6对synchronized的优化

你可能感兴趣的:(并发编程,java,多线程)