乐观锁、悲观锁以及CAS

一、基本概念

悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
Java中,synchronized关键字和Lock的实现类都是悲观锁。悲观锁适用于写操作多的应用场景,先加锁确保数据操作正确。

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁;
它是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁适用于读操作多的应用场景,不加锁可以大幅提高读的性能。

二、乐观锁与CAS

1、乐观锁实现
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的,例如AtomicInteger等。
2、CAS算法
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。

CAS算法涉及到三个操作数:

  1. 需要读写的内存值 V。
  2. 进行比较的值 A。
  3. 要写入的新值 B。

当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。

3、Java并发包实现

java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。事实上,这些类中具有三个重要属性:
1、unsafe对象:用来直接操作内存地址的数据
2、valueoffset:内存地址偏移量
3、value:包装类的值(volatile修饰)

对这些原子类的自增操作等实现如下:

// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
   int v;
   do {
       v = getIntVolatile(o, offset);
   } while (!compareAndSwapInt(o, offset, v, v + delta));
   return v;
}

可以看出该实现是一个自旋操作,首先直接获取对象中内存地址偏移量的值;然后执行compareAndSwapInt函数。该函数是一个native方法,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。

4、cmpxchg指令

事实上,cmpxchg指令也是含有锁的,只不过粒度非常小。它是借助了CPU的锁来保证数据操作的原子性,包括以下3种:
1、CPU自动保证基本内存操作的原子性
2、使用总线锁保证原子性
3、使用缓存锁保证原子性

5、计算机硬件内存模型

为什么使用这3三种锁就能保证数据操作的原子性呢?
这与计算机硬件级别的内存模型有关。CPU要操作RAM内存中的数据,首先需要将RAM中的数据读取到CPU的缓存(这就是买电脑时的一级、二级、三级缓存等),但是真正操作的数据其实是在CPU中的寄存器中,因此首先CPU可以保证基本内存操作的原子性。
然后总线管理CPU缓存与RAM之间的数据通信,因此使用总线锁可以保证RAM读写的原子性。
而缓存锁是因为使用总线锁的代价太大(因为一个CPU在写RAM数据时,其它CPU就不能操作RAM了);因此缓存锁是将RAM某一个数据对应的内存地址给锁住。这样就能保证数据操作的原子性,同时又能提高效率。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值