八、详解CAS无锁

目录

CAS

ABA问题

AtomicStampedReference

AtomicReferenceFieldUpdater

CAS底层原理

LongAdder(无锁+分段)

基本用法

缓存行

源码

Unsafe原理 

手写AtomicInteger


在之前的文章中,我们详细讲过了Synchronized三、详解Synchronized-CSDN博客和ReentrantLock五、详解ReentrantLock-CSDN博客的原理,以及他们如何实现线程安全的。

但是在这两种方式种,都是在不满足条件的时候将当前线程阻塞,直到被另一个线程唤醒。

这两种方式都需要切换线程上下文,而线程切换将消耗大量资源。

jdk提供了一种无锁的方式来保证线程之间安全。即循环CAS。

CAS

CAS(Compare-and-Swap,比较并交换)是一种用于实现无锁(lock-free)数据结构的原子操作。

它是一种乐观锁策略,通过比较内存中的值和预期值来决定是否更新内存中的数据。

CAS操作通常包含三个参数:

1、内存地址(或变量)、

2、预期值(或者说旧的值)

3、新值。

当内存地址中的值与旧值相等时,将内存地址中的值更新为新值,否则不进行任何操作。(说明地址中的值已经被别的线程修改了)

ABA问题

CAS存在一个著名的问题,被称为ABA问题。  

ABA问题是指,在CAS操作中,如果一个值原来为A,变为了B,然后又变回为A,那么在CAS检查时会发现该值没有发生变化,但是实际上却发生了变化。这就是所谓的ABA问题。  

举个例子,假设有两个线程A和B,线程A准备用CAS将变量的值从1改为2,线程B同时将变量的值从1改为3,然后又改回1。此时,线程A进行CAS操作,发现变量的值仍然为1,所以CAS操作成功。但实际上,这个变量的值已经被线程B改变过了。

AtomicStampedReference

 ABA问题的解决方案通常是使用版本号。在每次变量更新时,都让版本号加1。即使A经过变化又回到A,版本号也会有所不同。Java的AtomicStampedReference类就实现了这样的机制,它通过包装[E,Integer]的元组来对整数进行CAS操作,这个元组的第二位是版本号,通过版本号的变化来避免ABA问题。

public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
}

通过AtomicStampedReference的构造函数,可以看到其将数据和版本封装到了pair中。 这个Pair是AtomicStampedReference类中的一个内部类

private static class Pair {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static  Pair of(T reference, int stamp) {
            return new Pair(reference, stamp);
        }
}

在更新的时候需要4个参数:

1、期望值

2、新值

3、期望版本

4、新版本

AtomicReferenceFieldUpdater

AtomicReferenceFieldUpdater的主要作用是对某个类的实例的某个volatile字段进行原子操作,这样就可以避免使用昂贵的AtomicReference。在使用AtomicReferenceFieldUpdater时,需要提供目标类的Class对象、要更新的字段的类型的Class对象以及要更新的字段的名称。

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class AtomicReferenceFieldUpdaterDemo {
    static class User {
        volatile String name;
    }

    private static AtomicReferenceFieldUpdater updater =
            AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");

    public static void main(String[] args) {
        User user = new User();
        updater.compareAndSet(user, null, "Tom");
        System.out.println(user.name); // 输出:Tom
    }
}

在这个示例中,我们创建了一个User类,它有一个volatile字段name。然后我们使用AtomicReferenceFieldUpdater创建了一个更新器,用于对User实例的name字段进行原子操作。在main方法中,我们创建了一个User实例,并使用compareAndSet方法将name字段从null更新为"Tom"。

CAS底层原理

LongAdder(无锁+分段)

LongAdder是Java 8在java.util.concurrent.atomic包下新增的一个类,它提供了线程安全的加法操作。

LongAdder是为了高并发场景下的性能优化而设计的,相比于AtomicLong,它在高并发场景下有更好的性能,通过累加200w次的实验,LongAdder的效率是AtomicLong的4-5倍。

需要注意的是,LongAdder提供的是最终一致性,而不是即时一致性。也就是说,调用sum()方法获取的值可能不是最新的,因为其他线程可能还没有将自己的局部结果合并到全局结果中。如果需要即时一致性,应该使用AtomicLong。

其核心的思想是无锁+分段。所谓无锁就是cas去更新数据,分段就是利用Cells数组,将不同的线程打散到不同的Cell中。

基本用法

import java.util.concurrent.atomic.LongAdder;

public class LongAdderExample {
    private final LongAdder counter = new LongAdder();

    public void increment() {
        counter.increment();
    }

    public long get() {
        return counter.sum();
    }
}

LongAdder的源码核心变量有以下三个:
1、transient volatile Cell[] cells;    //默认2的n次方 用来分段计数

2、transient volatile long base;    //基本值,主要在没有争用时使用

3、transient volatile int cellsBusy;   //自旋锁(通过CAS锁定)在调整大小和/或创建Cell时使用。当cellsBusy==1的时候,表示加锁了

首先看一下Cell这个类的代码:

@jdk.internal.vm.annotation.Contended static final class Cell {

    volatile long value; //存储当前单元累加的变量

}

可以观察到当前class被一个@jdk.internal.vm.annotation.Contended注解注释了,此注解的主要作用是用于减少并发编程中的伪共享(False Sharing)问题。

伪共享是多线程编程中的一个问题,当多个线程修改互相独立的变量,但这些变量共享同一个缓存行时,就会导致性能下降。这是因为当一个线程修改了一个缓存行中的变量,整个缓存行都会被标记为无效,其他线程再访问同一个缓存行中的其他变量时,就需要重新从主内存中加载数据。

需要注意的是,@Contended注解是JDK内部的注解,不建议在普通的应用代码中使用。另外,这个注解的效果可能会因为JVM的具体实现和配置而有所不同。

缓存行

具体解释一下什么是缓存行,已经Cell在缓存行中是如何存储的:

现代计算机基本上都是多核cpu,那么不同cpu之间共享了同一块内存(主存),但是cpu和主存的计算速度是有差距的,因此在cpu和主存之间增加了多级cpu缓存(包括寄存器)。一般来讲缓存是以行为单位,每行缓存为64个byte。

那么在LongAdder中,按照对象大小的计算方式,一个Cell对象=markword+Klass pointer+实例对象,也就是8(64位机)+8(非压缩klasspointer)+8(long类型value) = 24byte。

由于缓存行为64byte,因此如果Cell数组长度小于为2的时候,Cell[0]和Cell[1]就有放在了同一个缓存行中,那么就有可能被存储在不同cpu的缓存行中。

八、详解CAS无锁_第1张图片

一旦线程1修改了Cell[0], 那么线程2也必须要刷新自己的缓存行。

如果将Cell[0]和Cell[1]都放到单独的缓存行中,那么线程1修改Cell[0], 不会影响线程2修改Cell[1]。

因此就利用@Contended注解,JVM会尽量将Cell对象与其他Cell隔离在不同的缓存行中,以减少伪共享的影响

源码

LongAdder的主要核心代码就在这个add方法中:

public void add(long x) {
        Cell[] cs; long b, v; int m; Cell c;
        if ((cs = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            if (cs == null || (m = cs.length - 1) < 0 ||
                (c = cs[getProbe() & m]) == null ||
                !(uncontended = c.cas(v = c.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
}

尝试分析:

1、一开始cells肯定是空的,因此第一个if不满足

2、开始对base进行cas,累加base的值。这个时候,如果是多线程进来,肯定有的线程成功,有的线程失败

3、如果casBase成功,则返回。如果失败。那就意味着一定是多线程在并发执行。

4、因此,cas失败的线程就执行if里面的逻辑,第二个if里面的逻辑cs肯定为空,那么就去执行longAccumulate方法。这个方法里面会创建cells,一会再说。

5、如果在其他线程已经创建好了cells的时候,有一个线程进来,第一个if一定满足,那么就判断第二个if,此时cells不是null,因此要判断  c = cs[getProbe() & m]) == null  也就是判断当前的线程是否已经有了对应的cell。如果没有,则执行longAccumulate,创建cell。如果不为null,那么就执行c.cas。 这个cas就是针对当前的线程的cell进行处理。 

八、详解CAS无锁_第2张图片

接下来看一下longAccumulate是如何创建累加单元cells的:
 

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
          //1. 首先,它会尝试在一个叫做cells的数组中找到一个位置,然后在这个位置上进行累加操作。用当前线程id
        if ((h = getProbe()) == 0) {
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        
        //上来就是一个死循环
        done: for (;;) {
            Cell[] cs; Cell c; int n; long v;

            //cells不为空的情况,也就是已经有线程创建过cells了

            if ((cs = cells) != null && (n = cs.length) > 0) {
                if ((c = cs[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        Cell r = new Cell(x);   // Optimistically create
                        if (cellsBusy == 0 && casCellsBusy()) {
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    break done;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else if (c.cas(v = c.value,
                               (fn == null) ? v + x : fn.applyAsLong(v, x)))
                    break;


                //如果线程cas失败,意味这个cell已经有线程占用了,就判断是否超过了cpu个数,用来设置collide=false 即不扩容
                else if (n >= NCPU || cells != cs)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;

                //cells是空的,且未加锁,并且当前线程加锁成功

                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        if (cells == cs)        // 创建扩容
                            cells = Arrays.copyOf(cs, n << 1);
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = advanceProbe(h);
            }

            //初始创建cells

            else if (cellsBusy == 0 && cells == cs && casCellsBusy()) {
                try {                           // Initialize table
                    if (cells == cs) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        break done;
                    }
                } finally {
                    cellsBusy = 0;
                }
            }

            // 线程针对自己的cell失败,则对base进行操作,如果对base失败,那么就直接for循环了

            else if (casBase(v = base,
                             (fn == null) ? v + x : fn.applyAsLong(v, x)))
                break done;
        }
    }

这个方法的主要逻辑是这样的:  

1. 首先,它会尝试在一个叫做cells的数组中找到一个位置,然后在这个位置上进行累加操作。  

2. 如果这个位置上没有元素,它会尝试创建一个新的元素并放到这个位置上。

 3. 如果这个位置上已经有元素,它会尝试对这个元素的值进行累加操作。

 4. 如果在进行累加操作时发生了竞争(也就是其他线程也在对这个元素进行操作),它会尝试扩大cells数组的大小,然后再次尝试进行累加操作。  

5. 如果cells数组已经达到了最大大小,或者扩大数组的操作失败,它会退回到使用一个叫做base的变量进行累加操作。

1、第一种情况,cells不存在

八、详解CAS无锁_第3张图片

2、 第二种情况,cells存在,但是当前线程的cell不存在

八、详解CAS无锁_第4张图片

3、第三种情况 ,cells存在且当前线程的cell存在

八、详解CAS无锁_第5张图片

Unsafe原理 

unsafe类是jdk中一个非常特殊的类,其内部大量的native方法来直接操作内存,如直接访问系统内存资源、线程调度等。这些操作在Java的安全模型中是被禁止的,因此它被命名为“Unsafe”,且本身unsafe类是一个单例的。其获取方式:

Unsafe unsafe = Unsafe.getUnsafe(); //单例

其主要功能是根据给定的对象,和偏移地址,来更新该地址上的数据且保证线程安全。

例如:

public static void main(String[] args) throws NoSuchFieldException {
        Unsafe unsafe = Unsafe.getUnsafe();

        //id相对Student类的偏移量
        long id = unsafe.objectFieldOffset(Student.class.getDeclaredField("id"));
        long name = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));
        Student student = new Student();
        unsafe.compareAndSwapInt(student, id, 0, 1);
        unsafe.compareAndSwapObject(student, name, null, "二牛");
        System.out.println(student.toString());
}

但是上述代码在运行的时候会报错:

Exception in thread "main" java.lang.SecurityException: Unsafe
	at jdk.unsupported/sun.misc.Unsafe.getUnsafe(Unsafe.java:99)

从异常上来看,提示一个线程安全的问题,这个主要是因为Unsafe类的设计初衷是为了系统类库的内部使用,所以它有一些访问限制。在JDK中,只有被Bootstrap ClassLoader加载的类才能通过Unsafe.getUnsafe()方法获取Unsafe实例。

具体可以看一下Unsafe.getUnsafe()代码的源码:

 public static Unsafe getUnsafe() {
        Class caller = Reflection.getCallerClass();
        //安全检查。这里会判断class loader
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
}


public static boolean isSystemDomainLoader(ClassLoader loader) {
        return loader == null || loader == ClassLoader.getPlatformClassLoader();
}

//获取平台class loader
public static ClassLoader getPlatformClassLoader() {
        SecurityManager sm = System.getSecurityManager();
        ClassLoader loader = getBuiltinPlatformClassLoader();
        if (sm != null) {
            checkClassLoaderPermission(loader, Reflection.getCallerClass());
        }
        return loader;
}


PLATFORM_LOADER = new PlatformClassLoader(BOOT_LOADER);

为了绕过这个限制,我们可以不使用unsafe内部的这个方法,而是使用反射的方式:
 

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        
        //获取内部属性
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        //直接使用反射
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        //id相对Student类的偏移量
        long id = unsafe.objectFieldOffset(Student.class.getDeclaredField("id"));
        long name = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));
        Student student = new Student();
        unsafe.compareAndSwapInt(student, id, 0, 1);
        unsafe.compareAndSwapObject(student, name, null, "二牛");
        System.out.println(student.toString());
}

由于Unsafe类提供的方法都是非常底层和危险的,一旦使用不当,可能会导致JVM崩溃或者数据不一致等严重问题,因此,对于大多数Java开发者来说,都不建议直接使用Unsafe类。

手写AtomicInteger

AtomicInteger本质上就是通过unsafe去操作一个int值,在对int进行修改的时候,循环无锁的方式。

public class AtomicTest {
    private Unsafe unsafe;
    private int value; //初始值
    private long offset; //value字段对应本类的偏移量

    public AtomicTest(int value) throws NoSuchFieldException, IllegalAccessException {
        
        //在构造函数中创建unsafe对象
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        this.unsafe = (Unsafe) theUnsafe.get(null);

        //设置偏移量
        this.offset = unsafe.objectFieldOffset(AtomicTest.class.getDeclaredField("value"));

        //设置初始value值
        this.value = value;
    }


    public void increment(){
         //value就是旧的值,value+1是新的值
        while(!unsafe.compareAndSwapInt(this, offset, value, value + 1)){
        }
    }

    //获取value
    public int get(){
        return value;
    }
}

你可能感兴趣的:(并发编程,java,jvm,开发语言)