并发-乐观锁、悲观锁

CAS

CAS原理

CAS:Compare and Swap(Compare and Swap 比较并交换)是乐观锁技术。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现

当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

  CAS操作中包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值(在CAS的一些特殊情况下将仅返回CAS是否成功,而不提取当前值)。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。这其实和乐观锁的冲突检查+数据更新的原理是一样的。

 

根据从上面的概念描述我们可以发现:

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

光说概念有些抽象,我们来看下乐观锁和悲观锁的调用方式示例:

 

JAVA对CAS的支持

Java原子类中的递增操作就通过CAS自旋实现的。

       在JDK1.5中新增java.util.concurrent包就是建立在CAS之上的。相对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以java.util.concurrent包中的AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解getAndIncrement方法,该方法的作用相当于++i操作。

 public class AtomicInteger extends Number implements java.io.Serializable{
   private volatile int value;
   public final int get(){
     return value;
   }
 
   /*
    *循环的内容是
    * 1.取得当前值
    * 2.计算+1后的值
    * 3.如果预期值等于 current 等于 内存值 this,值更新为 要修改的值 next
    * 4.如果设置没成功(当前值已经无效了即被别的线程改过了), 一直自旋
   */
   public final int getAndIncrement(){
     for (;;){
       int current = get();
       int next = current + 1;
       if (compareAndSet(current, next))
       return current;
     }
   }
  
   public final boolean compareAndSet(int expect, int update){
     return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
   }
 }
 

CAS三大问题

CAS虽然很高效,但是它也存在三大问题,这里也简单说一下:

1. ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。

2. 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。

3. 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。

Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

 

 

乐观锁、悲观锁

概念

锁分类

概述

使用场景

样例

悲观锁

悲观锁:对数据被外界修改持保守态度(悲观),因此在整个数据处理过程中,将数据处于锁定状态,往往依靠数据库提供的锁机制实现

写多读少,保证数据安全

行锁、页锁、表锁、共享锁、排他锁(写锁)

乐观锁

乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测。如果发现冲突了,则让返回用户错误的信息,让用户觉得如何去做或者程序自动取重试

读多写少,提高系统吞吐量

数据库乐观锁、缓存乐观锁

 

乐观锁、悲观锁到底谁的吞吐量大?

乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,吞吐量更高

 

乐观锁、悲观锁 高并发 选择?

在深入理解java虚拟机中提到,轻量级锁一般情况下是优于重量级锁(互斥锁)的;

如果在高并发锁竞争比较激烈的情况下轻量级锁会由于长时间自旋消耗cpu 从而使得轻量级锁的性能比传统的重量级锁更慢。

那么乐观锁中也有自旋和cas,所以高并发下乐观锁好像不是一种好的解决方案。

 

乐观锁怎么实现?

1.基于mysql通过版本号实现

2.基于mysql通过状态实现

(数据库乐观锁实现的有点在于简单高效、稳定可靠;缺点在于并发能力低)

update goods set amount = amount - #{buys},version = version + 1 where code = #{code} and version = #{version}

update goods set amount = amount - #{buys} where code = #{code} and amount - #{buys} >= 0

3.基于缓存的cas机制实现

基于redis的实现:利用watch指令在redis事务中提供CAS的能力

基于memcached的实现:1.2.4版本新增特性,通过gets和cas实现

1.1 读取数据

       |

1.2 比较版本

       |

1.3 更新数据

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章