知晓乐观锁的定义,乐观锁的使用、乐观锁的适用场景
乐观锁的定义
乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
乐观锁的实现
CAS(Compare And Swap)
CAS操作包括了3个操作数:
-
需要读写的内存位置(V)
-
进行比较的预期值(A)
-
拟写入的新值(B)
CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。
这里引出一个新的问题,既然CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?答案是:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。
问题:
cas还有aba问题,解决就是引入版本号,内存中值每改变一次版本号+1。
在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。当然,更重要的是避免在高竞争环境下使用乐观锁。
版本号机制
版本号机制也可以用来实现乐观锁。版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。
乐观锁的使用,场景
使用:
ConcurrentHashMap
ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它使用分离锁(Segment)来保证线程安全。每个Segment都是一个独立的哈希表,每个操作只锁定相关的Segment,因此可以支持更高的并发性。
ConcurrentHashMap使用了一种基于CAS的技术来实现乐观锁,它通过比较当前的value和预期的value是否相等来判断是否存在冲突。如果存在,则返回失败;如果不存在,则执行更新操作。
数据库并发控制
在日常开发中,使用乐观锁最常见的场景就是数据库的更新操作了。
为了保证操作数据库的原子性,我们常常会为每一条数据定义一个版本号,并在更新前获取到它,到了更新数据库的时候,还要判断下已经获取的版本号是否被更新过,如果没有,则执行该操作。
比如mysql
1.数据库表中添加锁标记字段
2.实体类中添加对应字段,并设定当前字段为逻辑删除标记字段
3.配置乐观锁拦截器实现锁机制对应的动态SQL语句拼装
4.使用乐观锁机制在修改前必须先获取到对应数据的verion方可正常进行
LongAdder
在 JDK1.8 中,Java 提供了一个新的原子类 LongAdder。LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好,代价就是会消耗更多的内存空间。
LongAdder 的原理就是降低操作共享变量的并发数,也就是将对单一共享变量的操作压力分散到多个变量值上,将竞争的每个写线程的 value 值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的 value 值进行 CAS 操作,最后在读取值的时候会将原子操作的共享变量与各个分散在数组的 value 值相加,返回一个近似准确的数值。
适用场景
乐观锁适用于以下场景:
读多写少的场景:在读操作远远多于写操作的情况下,乐观锁可以提高系统的并发性能。
冲突较少的场景:在数据冲突发生的概率较低的情况下,乐观锁可以减少因为锁争用而带来的性能开销。
无需长时间锁定资源的场景:乐观锁操作不需要长时间锁定资源,因此适用于需要尽快释放资源的场景。
需要注意的是,在并发量大、数据更新频繁、数据冲突概率较高的情况下,乐观锁可能会导致大量的重试操作,影响系统性能。因此,在选择乐观锁时,需要根据具体的业务场景进行评估和选择。