JUC笔记-共享模型之无锁


一、CAS

原理

无锁编程:Lock Free

CAS 的全称是 Compare-And-Swap,是 CPU 并发原语

底层原理:CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性。

特点

CAS 特点:

  • CAS 体现的是无锁并发无阻塞并发,线程不会陷入阻塞,线程不需要频繁切换状态(上下文切换,系统调用)
  • CAS 是基于乐观锁的思想

CAS 缺点:

  • 循环时间长,开销大,因为执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿使用 CAS 线程数不要超过 CPU的 核心数
  • 只能保证一个共享变量的原子操作
    • 对于一个共享变量执行操作时,可以通过循环 CAS 的方式来保证原子操作
    • 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候只能用锁来保证原子性
  • 引出来 ABA 问题 (如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化。)

CAS 与 synchronized 总结

  • synchronized 是从悲观的角度出发:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),因此 synchronized 也称之为悲观锁,ReentrantLock 也是一种悲观锁,性能较差
  • CAS是从乐观的角度出发:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。如果别人修改过,则获取现在最新的值,如果别人没修改过,直接修改共享数据的值,CAS 这种机制也称之为乐观锁,综合性能较好

二、问题提出

有如下需求,保证account.withdraw取款方法的线程安全,通过非阻塞式的乐观锁来实现并发控制

下面使用synchronized保证线程安全

/**
 * Description: 使用重量级锁synchronized来保证多线程访问共享资源发生的安全问题
 */
@Slf4j(topic = "guizy.Test1")
public class Test1 {

    public static void main(String[] args) {
        Account.demo(new AccountUnsafe(10000));
    }
}

class AccountUnsafe implements Account {
    private Integer balance;

    public AccountUnsafe(Integer balance) {
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
        synchronized (this) {
            return balance;
        }
    }

    @Override
    public void withdraw(Integer amount) {
        // 通过这里加锁就可以实现线程安全,不加就会导致线程安全问题
        synchronized (this) {
            balance -= amount;
        }
    }
}


interface Account {
    // 获取余额
    Integer getBalance();

    // 取款
    void withdraw(Integer amount);

    /**
     * Java8之后接口新特性, 可以添加默认方法
     * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
     * 如果初始余额为 10000 那么正确的结果应当是 0
     */
    static void demo(Account account) {
        List<Thread> ts = new ArrayList<>();
        long start = System.nanoTime();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        ts.forEach(thread -> thread.start());
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getBalance()
                + " cost: " + (end - start) / 1000_000 + " ms");
    }
}

解决思路-无锁

上面的代码中使用synchronized加锁操作来保证线程安全,但是 synchronized加锁操作太耗费资源 (因为底层使用了操作系统mutex指令, 造成内核态和用户态的切换),这里我们使用 无锁 来解决此问题

class AccountCas implements Account {
	//使用原子整数: 底层使用CAS+重试的机制
	private AtomicInteger balance;

	public AccountCas(int balance) {
		this.balance = new AtomicInteger(balance);
	}

	@Override
	public Integer getBalance() {
		//得到原子整数的值
		return balance.get();
	}

	@Override
	public void withdraw(Integer amount) {
		while(true) {
			//获得修改前的值
			int prev = balance.get();
			//获得修改后的值
			int next = prev - amount;
			//比较并设置值
			/*
				此时的prev为共享变量的值, 如果prev被别的线程改了.也就是说: 自己读到的共享变量的值 和 共享变量最新值 不匹配,
				就继续where(true),如果匹配上了, 将next值设置给共享变量.
				
				AtomicInteger中value属性, 被volatile修饰, 就是为了确保线程之间共享变量的可见性.
			*/
			if(balance.compareAndSet(prev, next)) {
				break;
			}
		}
	}
}

三、CAS 与 volatile (重点)

使用原子操作来保证线程访问共享资源的安全性,cas+重试的机制来确保(乐观锁思想), 相对于悲观锁思想synchronizedreentrantLock来说,cas的方式效率会更好。

3.1 cas + 重试 的原理

前面看到的AtomicInteger的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?

@Override
public void withdraw(Integer amount) {
    // 核心代码
    // 需要不断尝试,直到成功为止
    while (true){
        // 比如拿到了旧值 1000
        int prev = balance.get();
        // 在这个基础上 1000-10 = 990
        int next = prev - amount;
        /*
         compareAndSet 保证操作共享变量安全性的操作:
         ① 线程A首先获取balance.get(),拿到当前的balance值prev
         ② 根据这个prev值 - amount值 = 修改后的值next
         ③ 调用compareAndSet方法, 首先会判断当初拿到的prev值,是否和现在的
         	balance值相同;
         	3.1、如果相同,表示其他线程没有修改balance的值, 此时就可以将next值
         		设置给balance属性
         	3.2、如果不相同,表示其他线程也修改了balance值, 此时就设置next值失败, 
				然后一直重试, 重新获取balance.get()的值,计算出next值,
				并判断本次的prev和balnce的值是否相同...重复上面操作
		*/
        if (atomicInteger.compareAndSet(prev,next)){
            break;
        }
    }
}

其中的关键是compareAndSet比较并设置值),它的简称就是 CAS(也有 Compare And Swap 的说法),它必须是原子操作
在这里插入图片描述
流程 :

  • 当一个线程要去修改Account对象中的值时,先获取值prev(调用get方法),然后再将其设置为新的值next(调用cas方法)。在调用cas方法时,会将prevAccount中的余额进行比较。
    • 如果两者相等,就说明该值还未被其他线程修改,此时便可以进行修改操作。
    • 如果两者不相等,就不设置值,重新获取值prev(调用get方法),然后再将其设置为新的值next(调用cas方法),直到修改成功为止。

3.2 volatile的作用

  • 在上面代码中的AtomicInteger类保存值的value属性使用了volatile修饰。获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
  • volatile可以用来修饰 成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

注意: volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性

CAS 必须借助 volatile才能读取到共享变量的最新值来实现【比较并交换】的效果

3.3 为什么CAS+重试(无锁)效率高

  • 使用CAS+重试无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized会让线程在没有获得锁的时候,发生上下文切换,进入阻塞

  • 无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

四、Atomic

4.1 常用API

常见原子类:AtomicInteger、AtomicBoolean、AtomicLong

构造方法:

  • public AtomicInteger():初始化一个默认值为 0 的原子型 Integer
  • public AtomicInteger(int initialValue):初始化一个指定值的原子型 Integer

常用API:

方法作用
public final int get()获取 AtomicInteger 的值
public final int getAndIncrement()以原子方式将当前值加 1,返回的是自增前的值 i++
public final int incrementAndGet()以原子方式将当前值加 1,返回的是自增后的值 ++i
public final int getAndSet(int value)以原子方式设置为 newValue 的值,返回旧值
public final int addAndGet(int data)以原子方式将输入的数值与实例中的值相加并返回 实例:AtomicInteger 里的 value

4.2 原理分析

AtomicInteger 原理:自旋锁 + CAS 算法

CAS 算法:有 3 个操作数(内存值 V, 旧的预期值 A,要修改的值 B)

  • 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B
  • 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋

分析 getAndSet 方法:

  • AtomicInteger:

    public final int getAndSet(int newValue) {
        /**
        * this: 		当前对象
        * valueOffset:	内存偏移量,内存地址
        */
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
    

    valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据

    valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
    //调用本地方法   -->
    public native long objectFieldOffset(Field var1);
    
  • unsafe类:

    // val1: AtomicInteger对象本身,var2: 该对象值得引用地址,var4: 需要变动的数
    public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // var5: 用 var1 和 var2 找到的内存中的真实值
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));
    
        return var5;
    }
    

    var5:从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行 compareAndSwapInt() 再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据

  • 变量 value 用 volatile 修饰,保证了多线程之间的内存可见性,避免线程从工作缓存中获取失效的变量

    private volatile int value
    

    CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果

分析 getAndUpdate 方法

  • getAndUpdate:

    public final int getAndUpdate(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();	//当前值,cas的期望值
            next = updateFunction.applyAsInt(prev);//期望值更新到该值
        } while (!compareAndSet(prev, next));//自旋
        return prev;
    }
    

    函数式接口:可以自定义操作逻辑

    AtomicInteger a = new AtomicInteger();
    a.getAndUpdate(i -> i + 10);
    
  • compareAndSet:

    public final boolean compareAndSet(int expect, int update) {
        /**
        * this: 		当前对象
        * valueOffset:	内存偏移量,内存地址
        * expect:		期望的值
        * update: 		更新的值
        */
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    

4.3 原子引用

原子引用:对 Object 进行原子操作,提供一种读和写都是原子性对象引用变量

为了保证引用类型的共享变量是线程安全的(确保这个原子引用没有引用过别人)

原子引用类:AtomicReferenceAtomicStampedReference(原子更新带有版本号的引用类型,可以解决ABA问题)、AtomicMarkableReference(原子更新带有标记的引用类型)

AtomicStampedReferenceAtomicMarkableReference两者的区别

  • AtomicStampedReference 需要我们传入整型变量作为版本号,来判定是否被更改过
  • AtomicMarkableReference需要我们传入布尔变量作为标记,来判断是否被更改过

AtomicReference 类

  • 构造方法:AtomicReference<T> atomicReference = new AtomicReference<T>()
  • 常用 API:
    • public final boolean compareAndSet(V expectedValue, V newValue):CAS 操作
    • public final void set(V newValue):将值设置为 newValue
    • public final V get():返回当前值
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        Student s1 = new Student(33, "z3");
        
        // 创建原子引用包装类
        AtomicReference<Student> atomicReference = new AtomicReference<>();
        // 设置主内存共享变量为s1
        atomicReference.set(s1);

        // 比较并交换,如果现在主物理内存的值为 z3,那么交换成 l4
        while (true) {
            Student s2 = new Student(44, "l4");
            if (atomicReference.compareAndSet(s1, s2)) {
                break;
            }
        }
        System.out.println(atomicReference.get());
    }
}

class Student {
    private int id;
    private String name;
    //。。。。
}

4.4 原子数组

原子数组类:AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray

AtomicIntegerArray 类方法

/**
*   i		the index
* expect 	the expected value
* update 	the new value
*/
public final boolean compareAndSet(int i, int expect, int update) {
    return compareAndSetRaw(checkedByteOffset(i), expect, update);
}

4.5 原子更新器

保证多线程访问同一个对象的成员变量时,成员变量的线程安全性

原子更新器类:AtomicReferenceFieldUpdaterAtomicIntegerFieldUpdaterAtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域(Field)进行原子操作只能配合volatile修饰的字段使用,否则会出现异常 IllegalArgumentException: Must be volatile type

常用 API:

  • static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> c, String fieldName):构造方法
  • abstract boolean compareAndSet(T obj, int expect, int update):CAS
public class UpdateDemo {
    private volatile int field;
    
    public static void main(String[] args) {
        AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater
            		.newUpdater(UpdateDemo.class, "field");
        UpdateDemo updateDemo = new UpdateDemo();
        fieldUpdater.compareAndSet(updateDemo, 0, 10);
        System.out.println(updateDemo.field);//10
    }
}

4.6 原子累加器 (重要)

原子累加器类:LongAdderDoubleAdderLongAccumulatorDoubleAccumulator

LongAdder 和 LongAccumulator 区别

相同点

  • LongAddr 与 LongAccumulator 类都是使用非阻塞算法 CAS实现的,
  • LongAddr 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 提供了更强大的功能,可以自定义累加规则,当accumulatorFunctionnull时就等价于 LongAddr。

不同点

  • 调用 casBase 时,LongAccumulator 使用 function.applyAsLong(b = base, x) 来计算,LongAddr 使用 casBase(b = base, b + x)
  • LongAccumulator 类功能更加强大,构造方法参数中
    • accumulatorFunction是一个双目运算器接口,可以指定累加规则,比如累加或者相乘,其根据输入的两个参数返回一个计算值LongAdder 内置累加规则
    • identity则是 LongAccumulator 累加器的初始值,LongAccumulator 可以为累加器提供非0的初始值,而 LongAdder 只能提供默认的 0

LongAddr 性能比 AtomicLong 好

  • LongAddr性能提升的原因很简单,就是在有竞争时,设置多个累加单元(但不会超过cpu的核心数),Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。
  • 之前AtomicLong等都是在一个共享资源变量上进行竞争,while(true)循环进行CAS重试,性能没有LongAdder高

五、Addr

优化机制

LongAdder 是 Java8 提供的类,跟 AtomicLong 有相同的效果,但对 CAS 机制进行了优化,尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能

CAS 底层实现是在一个循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈修改成功率很高,否则失败率很高,失败后这些重复的原子性操作会耗费性能(导致大量线程空循环,自旋转

优化核心思想:数据分离,将 AtomicLong 的单点的更新压力分担到各个节点,空间换时间,在低并发的时候直接更新,可以保障和 AtomicLong 的性能基本一致,而在高并发的时候通过分段减少竞争,提高了性能

分段 CAS 机制

  • 在发生竞争时,创建 Cell 数组用于将不同线程的操作离散(通过 hash 等算法映射)到不同的节点上
  • 设置多个累加单元(会根据需要扩容,最大为 CPU 核数),Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1] 等,最后将结果汇总
  • 在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败从而提高性能

自动分段迁移机制:某个 Cell 的 value 执行 CAS 失败,就会自动寻找另一个 Cell 分段内的 value 值进行 CAS 操作


伪共享

Cell 为累加单元:数组访问索引是通过 Thread 里的 threadLocalRandomProbe 域取模实现的,这个域是 ThreadLocalRandom 更新的

// Striped64.Cell
@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    // 用 cas 方式进行累加, prev 表示旧值, next 表示新值
    final boolean cas(long prev, long next) {
    	return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
    }
    // 省略不重要代码
}

Cell 是数组形式,在内存中是连续存储的,64 位系统中,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象,当 Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致当前缓存行失效,从而导致对方的数据失效,需要重新去主存获取,影响效率

img

@sun.misc.Contended:防止缓存行伪共享,在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,使用 2 倍于大多数硬件缓存行让 CPU 将对象预读至缓存时占用不同的缓存行,这样就不会造成对方缓存行的失效

img


源码解析

Striped64 类成员属性:

// 表示当前计算机CPU数量
static final int NCPU = Runtime.getRuntime().availableProcessors()
// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 累加这个域,当 cells 扩容时,也会将数据写到 base 中
transient volatile long base;
// 在 cells 初始化或扩容时只能有一个线程执行, 通过 CAS 更新 cellsBusy 置为 1 来实现一个锁
transient volatile int cellsBusy;

工作流程:

  • cells 占用内存是相对比较大的,是惰性加载的,在无竞争或者其他线程正在初始化 cells 数组的情况下,直接更新 base 域
  • 在第一次发生竞争时(casBase 失败)会创建一个大小为 2 的 cells 数组,将当前累加的值包装为 Cell 对象,放入映射的槽位上
  • 分段累加的过程中,如果当前线程对应的 cells 槽位为空,就会新建 Cell 填充,如果出现竞争,就会重新计算线程对应的槽位,继续自旋尝试修改
  • 分段迁移后还出现竞争就会扩容 cells 数组长度为原来的两倍,然后 rehash,数组长度总是 2 的 n 次幂,默认最大为 CPU 核数,但是可以超过,如果核数是 6 核,数组最长是 8

方法分析:

  • LongAdder#add:累加方法

    public void add(long x) {
        // as 为累加单元数组的引用,b 为基础值,v 表示期望值
        // m 表示 cells 数组的长度 - 1,a 表示当前线程命中的 cell 单元格
        Cell[] as; long b, v; int m; Cell a;
        
        // cells 不为空说明 cells 已经被初始化,线程发生了竞争,去更新对应的 cell 槽位
        // 进入 || 后的逻辑去更新 base 域,更新失败表示发生竞争进入条件
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            // uncontended 为 true 表示 cell 没有竞争
            boolean uncontended = true;
            
            // 条件一: true 说明 cells 未初始化,多线程写 base 发生竞争需要进行初始化 cells 数组
            //		  fasle 说明 cells 已经初始化,进行下一个条件寻找自己的 cell 去累加
            // 条件二: getProbe() 获取 hash 值,& m 的逻辑和 HashMap 的逻辑相同,保证散列的均匀性
            // 		  true 说明当前线程对应下标的 cell 为空,需要创建 cell
            //        false 说明当前线程对应的 cell 不为空,进行下一个条件【将 x 值累加到对应的 cell 中】
            // 条件三: 有取反符号,false 说明 cas 成功,直接返回,true 说明失败,当前线程对应的 cell 有竞争
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
            	// 【uncontended 在对应的 cell 上累加失败的时候才为 false,其余情况均为 true】
        }
    }
    
  • Striped64#longAccumulate:cell 数组创建

    							// x  			null 			false | true
    final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
        int h;
        // 当前线程还没有对应的 cell, 需要随机生成一个 hash 值用来将当前线程绑定到 cell
        if ((h = getProbe()) == 0) {
            // 初始化 probe,获取 hash 值
            ThreadLocalRandom.current(); 
            h = getProbe();	
            // 默认情况下 当前线程肯定是写入到了 cells[0] 位置,不把它当做一次真正的竞争
            wasUncontended = true;
        }
        // 表示【扩容意向】,false 一定不会扩容,true 可能会扩容
        boolean collide = false; 
        //自旋
        for (;;) {
            // as 表示cells引用,a 表示当前线程命中的 cell,n 表示 cells 数组长度,v 表示 期望值
            Cell[] as; Cell a; int n; long v;
            // 【CASE1】: 表示 cells 已经初始化了,当前线程应该将数据写入到对应的 cell 中
            if ((as = cells) != null && (n = as.length) > 0) {
                // CASE1.1: true 表示当前线程对应的索引下标的 Cell 为 null,需要创建 new Cell
                if ((a = as[(n - 1) & h]) == null) {
                    // 判断 cellsBusy 是否被锁
                    if (cellsBusy == 0) {   
                        // 创建 cell, 初始累加值为 x
                        Cell r = new Cell(x);  
                        // 加锁
                        if (cellsBusy == 0 && casCellsBusy()) {
                            // 创建成功标记,进入【创建 cell 逻辑】
                            boolean created = false;	
                            try {
                                Cell[] rs; int m, j;
                                // 把当前 cells 数组赋值给 rs,并且不为 null
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    // 再次判断防止其它线程初始化过该位置,当前线程再次初始化该位置会造成数据丢失
                                    // 因为这里是线程安全的判断,进行的逻辑不会被其他线程影响
                                    rs[j = (m - 1) & h] == null) {
                                    // 把新创建的 cell 填充至当前位置
                                    rs[j] = r;
                                    created = true;	// 表示创建完成
                                }
                            } finally {
                                cellsBusy = 0;		// 解锁
                            }
                            if (created)			// true 表示创建完成,可以推出循环了
                                break;
                            continue;
                        }
                    }
                    collide = false;
                }
                // CASE1.2: 条件成立说明线程对应的 cell 有竞争, 改变线程对应的 cell 来重试 cas
                else if (!wasUncontended)
                    wasUncontended = true;
                // CASE 1.3: 当前线程 rehash 过,如果新命中的 cell 不为空,就尝试累加,false 说明新命中也有竞争
                else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                    break;
                // CASE 1.4: cells 长度已经超过了最大长度 CPU 内核的数量或者已经扩容
                else if (n >= NCPU || cells != as)
                    collide = false; 		// 扩容意向改为false,【表示不能扩容了】
                // CASE 1.5: 更改扩容意向,如果 n >= NCPU,这里就永远不会执行到,case1.4 永远先于 1.5 执行
                else if (!collide)
                    collide = true;
                // CASE 1.6: 【扩容逻辑】,进行加锁
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        // 线程安全的检查,防止期间被其他线程扩容了
                        if (cells == as) {     
                            // 扩容为以前的 2 倍
                            Cell[] rs = new Cell[n << 1];
                            // 遍历移动值
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            // 把扩容后的引用给 cells
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;	// 解锁
                    }
                    collide = false;	// 扩容意向改为 false,表示不扩容了
                    continue;
                }
                // 重置当前线程 Hash 值,这就是【分段迁移机制】
                h = advanceProbe(h);
            }
    
            // 【CASE2】: 运行到这说明 cells 还未初始化,as 为null
            // 判断是否没有加锁,没有加锁就用 CAS 加锁
            // 条件二判断是否其它线程在当前线程给 as 赋值之后修改了 cells,这里不是线程安全的判断
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                // 初始化标志,开始 【初始化 cells 数组】
                boolean init = false;
                try { 
                   	// 再次判断 cells == as 防止其它线程已经提前初始化了,当前线程再次初始化导致丢失数据
                    // 因为这里是【线程安全的,重新检查,经典 DCL】
                    if (cells == as) {
                        Cell[] rs = new Cell[2];	// 初始化数组大小为2
                        rs[h & 1] = new Cell(x);	// 填充线程对应的cell
                        cells = rs;
                        init = true;				// 初始化成功,标记置为 true
                    }
                } finally {
                    cellsBusy = 0;					// 解锁啊
                }
                if (init)
                    break;							// 初始化成功直接跳出自旋
            }
            // 【CASE3】: 运行到这说明其他线程在初始化 cells,当前线程将值累加到 base,累加成功直接结束自旋
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break; 
        }
    }
    
  • sum:获取最终结果通过 sum 整合,保证最终一致性,不保证强一致性

    public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            // 遍历 累加
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }
    

累加流程图
在这里插入图片描述


六、ABA(重点)

ABA 问题:当进行获取主内存值时,该内存值在写入主内存时已经被修改了 N 次,但是最终又改成原来的值

其他线程先把 A 改成 B 又改回 A,主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,这时 CAS 虽然成功,但是过程存在问题

6.1 AtomicStampedReference (加版本号解决ABA问题)

  • 构造方法:
    • public AtomicStampedReference(V initialRef, int initialStamp):初始值和初始版本号
  • 常用API:
    • public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp):期望引用和期望版本号都一致才进行 CAS 修改数据
    • public void set(V newReference, int newStamp):设置值和版本号
    • public V getReference():返回引用的值
    • public int getStamp():返回当前版本号
public static void main(String[] args) {
    AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<>(100,1);
    int startStamp = atomicReference.getStamp();
    new Thread(() ->{
        int stamp = atomicReference.getStamp();
        atomicReference.compareAndSet(100, 101, stamp, stamp + 1);
        stamp = atomicReference.getStamp();
        atomicReference.compareAndSet(101, 100, stamp, stamp + 1);
    },"t1").start();

    new Thread(() ->{
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (!atomicReference.compareAndSet(100, 200, startStamp, startStamp + 1)) {
            System.out.println(atomicReference.getReference());//100
            System.out.println(Thread.currentThread().getName() + "线程修改失败");
        }
    },"t2").start();
}

6.2 AtomicMarkableReference (标记cas的共享变量是否被修改过)

  • AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A -> B -> A ->C,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。
  • 但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference
public class Test1 {

    static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", true);

    public static void main(String[] args) {
        new Thread(() -> {
            String pre = ref.getReference();
            System.out.println("change");
            try {
                other();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println("change A->C mark " + ref.compareAndSet(pre, "C", true, false));
        }).start();
    }

    static void other() throws InterruptedException {
        new Thread(() -> {
            System.out.println("change A->A mark " + ref.compareAndSet(ref.getReference(), "A", true, false));
        }).start();
    }
}

在这里插入图片描述

七、Unsafe

  • Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得
  • 可以发现AtomicInteger以及其他的原子类底层都使用的是Unsafe类

模拟实现原子整数:

public static void main(String[] args) {
    MyAtomicInteger atomicInteger = new MyAtomicInteger(10);
    if (atomicInteger.compareAndSwap(20)) {
        System.out.println(atomicInteger.getValue());
    }
}

class MyAtomicInteger {
    private static final Unsafe UNSAFE;
    private static final long VALUE_OFFSET;
    private volatile int value;

    static {
        try {
            // 需要反射获取
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
            // 获取 value 属性的内存地址,value 属性指向该地址,直接设置该地址的值可以修改 value 的值
            VALUE_OFFSET = UNSAFE.objectFieldOffset(
                		   MyAtomicInteger.class.getDeclaredField("value"));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    public MyAtomicInteger(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }

    public boolean compareAndSwap(int update) {
        while (true) {
            int prev = this.value;
            int next = update;
            //							当前对象  内存偏移量    期望值 更新值
            if (UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, update)) {
                System.out.println("CAS成功");
                return true;
            }
        }
    }
}

参考:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值