悲观锁与乐观锁
悲观锁
定义:对于同一数据的并发操作,悲观锁认为当我在使用数据的时候一定有其他线程也会来访问和修改数据,因此在获取数据的时候会加锁,确保数据不会被别的线程修改。
应用:Java中,synchronized关键字和Lock的实现类都是悲观锁。
乐观锁
定义:乐观锁认为在对访问数据进行时,没有别的线程来修改数据,所以不需要加锁。但是为了确保只有自己在对数据进行了操作,自己在更新数据的时候会去判断以前有没有别的线程对数据进行了修改。如果数据没有更新,当前线程会将自己修改的数据成功写入;如果数据被更新了,则根据不同的实现方式执行不同的操作(例如报错和循环重试)
应用:java的原子类的自增操作就是通过CAS自旋算法实现的。
应用场景
悲观锁适合
写操作多的场景,先加锁可以保证数据正确性。乐观锁适合
读操作多的场景,不加锁特性可以提高读操作的性能。
CAS
定义:CAS全称是Compare And Swap (cmpxchg指令),它是一种无锁的算法,在不使用锁(没有线程阻塞)的情况下实现多线程间的变量同步。由于它实现的特性,也叫做自旋锁、乐观锁。
源码分析
1 | count.incrementAndGet(); // 替代count++ |
AtomicInteger.incrementAndGet方法调用了unsafe.getAndAddInt方法
1 | public final int incrementAndGet() { |
看一下AtomicInteger的定义
1 | public class AtomicInteger extends Number implements java.io.Serializable { |
根据定义我们可以看出各属性的作用:
- unsafe: 获取并操作内存的数据。
- valueOffset: 存储value在AtomicInteger中的偏移量。
- value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。
接下来查看Unsafe.getAndAddInt方法的实现
1 | public final int getAndAddInt(Object var1, long var2, int var4) { |
分析源码可以看到:
- getIntVolatile方法获取给定对象o中的偏移量处的值var5
- 然后调用compareAndSwapInt方法,
比较+更新是个原子操作,这是通过CPU的cmpxchg指令去实现的 - compareAndSwapInt就是cas操作,可看作cas(V,Excepted,NewValue),V就是要修改的值,Excepted是当前的期望值,NewValue是修改后的值。
- 当V==Excepted时,将V更新为NewValue,否则,返回flase,继续while循环再次调用cmpxchg指令进行重试,直到成功为止。
CAS带来的问题
ABA问题
描述:因为CAS需要在操作值时候,检查值有没有发生变化,如果没有发生变化,但是如果一个值原来是A,变成了原来的B,又变成了A,但是用CAS检查出来它没有变化,但是实际上是变化了的。
影响:如果是基础类型,不产生影响。如果是引用类型,可能会产生一些影响,这个影响对业务来说可能不可忽略。
解决思路:在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”
- JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
AtomicStampedReference.compareAndSet方法的实现
1 | public boolean compareAndSet(V expectedReference, |
循环时间长开销大
描述:CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
只能保证一个共享变量的原子操作
描述:对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
- Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
AtomicReference类的定义,使用了泛型
1 | public class AtomicReference<V> implements java.io.Serializable { |