CAS(理解)AtomicInteger(源码)

首先理解悲观锁和乐观锁:

乐观锁:

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。


悲观锁:
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。

CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。(有点像自旋锁)

CAS主要是三个操作:

1.获取当前值

2.当前值加一赋给目标值

3.进行CAS操作,成功跳出循环,失败就重复上述操作

import java.util.ArrayList;  
import java.util.List;  
import java.util.concurrent.atomic.AtomicInteger;  
  
public class Counter {  
    private AtomicInteger ai = new AtomicInteger();  
    private int i = 0;  
  
    public static void main(String[] args) {  
        final Counter cas = new Counter();  
        List<Thread> ts = new ArrayList<Thread>();  
        // 添加100个线程  
        for (int j = 0; j < 100; j++) {  
            ts.add(new Thread(new Runnable() {  
                public void run() {  
                    // 执行100次计算,预期结果应该是10000  
                    for (int i = 0; i < 100; i++) {  
                        cas.count();  
                        cas.safeCount();  
                    }  
                }  
            }));  
        }  
        //开始执行  
        for (Thread t : ts) {  
            t.start();  
        }  
        // 等待所有线程执行完成  
        for (Thread t : ts) {  
            try {  
                t.join();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
        System.out.println("非线程安全计数结果:"+cas.i);  
        System.out.println("线程安全计数结果:"+cas.ai.get());  
    }  
  
    /** 使用CAS实现线程安全计数器 */  
    private void safeCount() {  
        for (;;) {  
            int i = ai.get();  
            // 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值  
if (ai.compareAndSet(i, ++i)) {
System.out.println(i);//并不是按顺序输出的,但所有的值都会输出来
break;

}
        }  
    }  
  
    /** 非线程安全计数器 */  
    private void count() {  
        i++;  
    }  
}  
//结果:  
非线程安全计数结果:9867  
线程安全计数结果:10000

CAS操作中的三个问题:

1.ABA问题:

例子:

小灰有100元存款,要用一个提款机来提款50元。

由于提款机硬件出现问题,小灰提款操作被提交两次,开启两个线程,两个线程都是获取当前值100元,要更新成50元。

理想情况下是一个线程更新成功一个线程更新失败,只扣一次。

线程1(提款机): 获取当前值100元,成功更新成50元

线程2(提款机): 获取当前值100元,期望更新为50,BLOCK

线程3(小灰妈): 获取当前值50元,期望更新为100元

在线程1和3执行完了以后,compare以后线程2会再次执行,就会执行两次扣钱。

如何解决这个问题?

除了比较期望值以外还要比较变量的版本号,在线程1操作的时候期望值100,版本号a01,在线程3操作完以后,期望值虽然还是变成了100,但是版本号变为了a03,在线程2去操作的时候发现版本号不一致了(之前保存的版本号是a01),就不会执行这个操作了

在Java底层源码里面有一个类AtomicStampedReference类就是解决ABA问题的 ,但它保存的是每次修改数据的数据段额时间戳,在每次修改的时候出来比较原始数据是否相等以外,还要比较时间戳。

参考:https://www.sohu.com/a/215510186_465221

2.循环时间开销大的问题:

观察上面的代码,如果在循环的过程中长时间无法成功不能退出,那么会给cpu带来非常大的执行开销

3.只能保证一个共享变量的操作:

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

参考:https://286.iteye.com/blog/2295165

 引申:关于总线锁和缓存锁的理解?

AtomicInteger的底层源码实现:通过Unsafe这个类,计算实例对象在内存中的偏移量

// 使用 unsafe 类的原子操作方式
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        //计算变量 value 在类对象中的偏移量
        valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

Unsafe内部实现:

//volatile变量value
private volatile int value;

 /**
 * 创建具有给定初始值的新 AtomicInteger
 *
 * @param initialValue 初始值
 */
public AtomicInteger(int initialValue) {
    value = initialValue;
}

//返回当前的值
public final int get() {
    return value;
}
//原子更新为新值并返回旧值
public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}
//最终会设置成新值
public final void lazySet(int newValue) {
    unsafe.putOrderedInt(this, valueOffset, newValue);
}
//如果输入的值等于预期值,则以原子方式更新为新值
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//方法相当于原子性的 ++i
public final int getAndIncrement() {
    //三个参数,1、当前的实例 2、value实例变量的偏移量 3、递增的值。
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
//方法相当于原子性的 --i
public final int getAndDecrement() {
    //三个参数,1、当前的实例 2、value实例变量的偏移量 3、递减的值。
    return unsafe.getAndAddInt(this, valueOffset, -1);
}

Unsafe类可以执行以下几种操作:

  1. 分配内存,释放内存:在方法allocateMemory,reallocateMemory,freeMemory中,有点类似c中的malloc,free方法
  2. 可以定位对象的属性在内存中的位置,可以修改对象的属性值。使用objectFieldOffset方法(这里我们主要使用这个功能)
  3. 挂起和恢复线程,被封装在LockSupport类中供使用
  4. CAS操作(CompareAndSwap,比较并交换,是一个原子操作)
posted @ 2019-03-05 19:05  LeeJuly  阅读(217)  评论(0编辑  收藏  举报