乐观锁

乐观锁

Posted by ZhaoLe on October 15, 2019

概念 乐观锁认为自己在使用数据的时候不会有别的线程修改数据,所以不会添加锁的,只有在更新数据的时候判断之前有没有别的线程更新这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

实现 java.util.concurrent.atomic包下面原子变量类就是乐观锁的一种实现方式

优势 乐观锁在java中使用无锁编程来实现,常用CMS算法。 相比较于悲观锁来说,不会带来死锁,饥饿等活性故障问题,线程间相互影响也比悲观锁小,更重要的是:乐观锁因为没有因竞争造成的系统开销,所以性能上也是更胜一筹 当多个线程同时操作一个共享资源时候,只有一个线程会成功,失败的线程不会像悲观锁那样挂起,仅仅是返回,并且系统允许失败的线程重试,也允许自动放弃退出操作。

使用场景 乐观锁适合读多写少的场景。

CMS算法(Compare And Swap) CMS是乐观锁的核心算法,它包含三个参数,V:共享变量的内存地址,E:期望值(比较值),B:要写入的新值。 当且仅当V=A成立,CMS算法通过原子方式将B更新V,否则不会执行任何操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ----------------- JDK 8 -----------------
// AtomicInteger 自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
      var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

// ----------------- 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;
}

获取对象O中的偏移量处的值V,然后断内存中的值是否相等,如果相等设置 v + delta,如果不相等则循环重试,直到设置成功才能退出循环,并且将旧值返回。

缺点

  • ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
    • JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
  • 循环时间长开销大。它不能代替堵塞,自旋锁虽然避免了线程切换开销,CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
  • 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
    • Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

乐观锁的实现

1.自旋锁

概念 当一个线程去获取锁的时候,如果锁已经被其他线程获取,那么该线程会循环等待,然后不断的判断锁是否能够被成功获取,直到获取锁才会退出

优势 不会使线程状态发生切换,线程一直都是activce的;不会使线程进入堵塞状态,减少不必要的上下文切换。

实现方式 自旋锁的实现原理同样也是CAS,源码中使用do-while循环进行自旋。

1
2
3
4
5
6
7
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;
}
2.适应性自旋锁

JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

文章引用