完全看懂CAS之JDK并发包JUC里cas使用volatile变量自旋的乐观锁模式解决多线程单变量同步问题与CAS缺点ABA资源消耗

当谈到cas解决多线程同步问题,看一个人有没有真正理解cas,不是听他说cas就是使用cpu机器指令比较前后值完成设置保证了原子性,这样基本就是百度百出来的,根本没有好好理解cas的真正过程

【cas要点】

  1. cas方法中的同步变量【必须】是volatile类型
  2. cas自旋(死循环),判断值前后变化来保证多线程中的值同步

JUC就是jdk并发包【目录简称

以AtomicInteger内部源码举例,jdk1.8.0_202

AtomicInteger.java
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    
unsafe.java
    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;
    }

也有的显示源码是这样的

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

AtomicInteger会在类的字节码文件加载到jvm时,执行static静态块初始化,取得内部value变量偏移长度

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

value是volatile类型,value就是AtomicInteger代表的值

private volatile int value;

然后主要是看cas循环怎么解决多线程变量同步问题,就是怎么解决 i++ 这样的问题的,这样才能真正理解cas的无锁同步机制

循环的过程主要是两步,可以理解为两个原子操作

1. 获取当前值【最新】值,volatile属性
int current = get(); 
或者
//var1是实例对象this,var2是valueOffset就是value的偏移长度,这样也可以得到value最新值
var5 = this.getIntVolatile(var1, var2); 

2. 判断与修改值
 含义:通过对象this与变量偏移地址valueOffset取得变量值
 //如果与预期expect (就是上面的原子操作取得的值) 一致,说明这两步原子操作中间【没有】被【并发修改】,可以设置为update
 //与预期expect不一致,进入下次循环。
 //说明value被其他线程修改了,为了保证变量同步,应该重新获取一次最新值
 //也就是当前线程的这两个原子操作中间被中断,线程挂起,被其他线程执行了cas设置
 //通过对象this与变量偏移地址valueOffset可以获取最新被修改后的值 (volatile属性)(这是在cas内部处理)
 //expect就是第一个原子操作取得的current值, 用于校验这两个原子操作是否被中断,从而保证【多线程单变量同步】
 
unsafe.compareAndSwapInt(this, valueOffset, expect, update) 
或者
this.compareAndSwapInt(var1, var2, var5, var5 + var4)

从源码以及我上面的说明可以看出,cas自旋一次的过程是两个原子操作

  1. 没有并发时,就直接cas比较成功,然后设置成功
  2. 有并发时,变量被并发线程修改,会导致cas内部通过偏移地址valueOffset获取的最新值与第一个原子操作的取得值不一致,从而判断出变量已被其他线程修改,为保证多线程共享变量同步,进入下次循环,获取新的值,继续进行一次比较,直到没有被其他线程修改,设置成功。可以看出这里有多线程争用,只有一个线程可以cas成功,其他线程在不断的循环重试cas

CAS问题

  1. 只能保证一个volatile变量的无锁同步
即使使用多个cas设置多个volatile变量,但是不能保证这些多个volatile变量被同时在一起修改的原子性
没有原子性,就会出现并发问题,变量值不正确。
  1. ABA问题
虽然CAS保证了两个原子操作的前后的【值】一样,但是不能保证这个值没有被重新设置为【一样】的值
比如这个值可能是某个链表的指针,虽然地址是一样,执行同一个地方,但是你不知道,在第一个原子操作后,线程挂起
其他线程修改了这个链表后,恰巧把相同的地址放在这里,等你第二个原子操作开始,虽然【值】是一样的
但是这个【新值】所指的含义,或者这个【新值】本身的含义不同于原来的【旧值】
  1. 性能问题
从我上面的分析,看出,虽然cas无锁同步,保证了多线程下的单变量同步
但当需要cas同一变量的线程太多,因为同一时刻,只有一个线程可以cas成功
余下的线程都在循环cas,线程越多,争用越大,循环时间就长,消耗cpu时间

你可能感兴趣的:(java)