乐观锁和悲观锁详述和应用场景
乐观锁和悲观锁原理和应用场景
1、并发控制
在多线程出现并发的情况时,为了保证并发情况数据的准确和安全性,就需要通过并发控制保证一个用户的操作不会对另一个用户的操作产生不合理的影响。没有做好并发控制,就可能会导致脏读、幻读和不可重复读等问题。
首先要明确:无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。
2、乐观锁
它乐观的认为,在操作数据时,别的线程不会更改数据,所以只是在更新数据前,判断数据是否被别的线程更新过。没有被更新的话,就成功写入;被更新过就放前当前更新操作,可以相对应的做报错或者自动重试。一般的实现乐观锁的方式就是记录数据版本。
乐观锁只是判断数据是否被更新,所以实际上是不会上锁的。
3、悲观锁
它则是悲观的认为,每次线程操作数据的时候一定会有别的线程修改数据,所以要通过加锁来将资源锁定,再进行数据的修改,确保万无一失。
借助锁机制,在数据修改前将数据资源先锁定,再让获取锁的线程去修改,也叫悲观并发控制。
Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
这里网上找到一个图例来看一下悲观锁和乐观锁的流程区别:
4、实现方式
悲观锁就形如synchronized来加锁的机制一样,这里主要来探讨一下乐观锁的实现。
乐观锁的主要实现方式"CAS"的技术原理
CAS全称 Compare and Swap(比较与交换),是一种无锁算法。在不使用锁的情况下(没有线程被阻塞),实现多线程的变量同步。
CAS算法主要涉及到3个操作数:
①需要进行读写操作的值 V
②判断是否更新的比较值 A
③需要替换值V写入新的值 B
在最开始将V赋值给A,再进行数据操作生成B,需要更新V之前,比较一下A是否还与V相等,相等说明数据未变化,执行更新操作,不相等,则更新不能完成。
但是,虽然CAS效率很高,但是有时候会出现三大问题:
1)ABA问题
CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是发生了变化的。
ABA问题的解决思路就是添加版本号。乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。
除了上述添加版本号以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。
2)循环时间长开销大
CAS操作如果长时间不成功,会导致其一直自旋(一般更新不成功会重复尝试),给CPU带来非常大的开销,性能下降。
对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。所以,还是要想办法减少乐观锁的粒度的。有一条比较好的建议,可以减小乐观锁力度,最大程度的提升吞吐率,提高并发能力!
高并发环境下锁粒度把控是一门重要的学问,选择一个好的锁,在保证数据安全的情况下,可以大大提升吞吐率,进而提升性能。(待补充。。。)
3)只能保证一个共享变量的原子操作
对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
5、如何选择应用场景
乐观锁和悲观锁看待线程并行的思想不同,对应合适的应用场景也不同。
乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败,适合于读操作多的场景。
悲观锁依赖锁机制,效率低。但是更新失败的概率也比较低,所以悲观锁合适写操作多的场景。