Java中的线程进阶:CAS和原子类

一、CAS

1. 问题引入

  • 假设现银行账户中存在10000元,现在有1000个线程同时来操作这些余额,每个线程要减少10元,那么,当1000个线程运行完成后,账户中余额会是预想中的0元吗?根据之前所学习的知识可以知道,显然不会,因为多个线程操作共享变量必然会存在线程安全问题。
  • 我们应该如何去解决线程安全问题?① 方法一(不举例):加synchronized锁,让同一时刻只能有一个线程去操作共享变量。② 方法二(如下代码):采用Java中提供的原子类AtomicInteger来存余额,利用boolean compareAndSet(int expect, int update)方法来进行余额的更改,该方法传入两个参数分别为为更改前的原值和更改后的目标值,返回值为是否更改成功,如果更改成功则退出循环,否则无限循环进行更改操作。
    //方法二
    class AccountSafe implements Account {
    	private AtomicInteger balance;
    	public AccountSafe(Integer 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;
    			//比较并设置
    			if (balance.compareAndSet(prev, next)) {
    				break;
    			}
    		}
    	}
    }
    

2. 方法解析

上述的方法二的关键就是compareAndSet,它的简称就是CAS,为什么该方法可以保证线程安全呢?
① compareAndSet方法在CPU的底层实现中使用了lock cmpxchg指令(X86架构),保证了该方法执行的原子性,即该方法的运行过程不会被其他线程所打断。
② compareAndSet方法是对原子类中的volatile关键字修饰的变量进行的更改,直接对主存中的数据进行了更改,利用读写屏障保证了该变量的可见性并维持了代码执行的有序性。CAS必须借助volatile才能读到共享变量的最新值。
在这里插入图片描述

3. CAS特点

  • CAS的方式进行解决时,即使重试失败,线程始终在高速运行,没有停歇。它不会像synchronized一样让线程在没有获得锁的时候发生上下文切换进入阻塞。因此,CAS方式的效率要高一些。
  • CAS方式是基于乐观锁的思想实现的,它不怕别的线程来修改共享变量,会在每次更新值的时候进行比较来决定是否更新。
  • CAS方式体现的是无锁并发、无阻塞并发。无锁即不加锁,无阻塞即不等待。
  • 注意:CAS方式需要额外的CPU支持,因为CAS方式的自旋需要不断地在CPU上运行,因此CAS方式适用于线程数少、多核CPU的场景。

4. ABA问题

4.1 基本概念

  • 线程A利用CAS对某个变量i进行修改之前,其他线程将i的值改完之后又改回去了,此时线程A无法察觉到i的值已经发生过变化,依然会对变量i按照原定的方式进行修改。

4.2 解决方式

  • 如果是自己定义的CAS自旋锁,则可以加版本号或时间戳。
  • 如果是使用的原子类,则可以使用更加安全的其他原子类。

二、Java中的原子类

1. 原子整数

1.1 AtomicInteger

方法作用
get()得到当前值
incrementAndGet()先加1后获取
getAndIncrement()先获取后加1
getAndAdd(int num)先加num后获取
addAndGet(int num)先获取后加num
updateAndGet(IntUnaryOperator updateFunction)传入参数为函数式接口,先按照传入的规则进行更新再获取
getAndUpdate(IntUnaryOperator updateFunction)传入参数为函数式接口,先获取再按照传入的规则进行更新

1.2 AtomicLong

1.3 AtomicBoolean

2. 原子引用

2.1 AtomicReference

  • 原子整数类只能用于保障基本数据类型,原子引用类就用于保障引用数据类型。

2.2 AtomicStampedReference

  • 与AtomicReference相比,AtomicStampedReference增加了一个版本号,解决了ABA的问题。

2.3 AtomicMarkableReference

  • 我们并不关心其他线程对引用变量更改了几次,只关心是否有线程对变量进行了更改,因此定义了了新的原子引用类。它采用一个布尔值来代替版本号,布尔值用来记录变量的更改情况。

3. 原子数组

原子引用类只能对整个引用进行保护,不能保护引用中的值,因此,定义了原子数组类,对数组中每个元素都进行保护。

3.1 AtomicReferenceArray

3.2 AtomicIntegerArray

3.3 AtomicLongArray

4. 字段更新器

字段更新器,主要是用于针对对象的某个属性进行原子操作,保障线程安全,要求该对象的属性必须为volatile的。

4.1 AtomicReferenceFieldUpdater

4.2 AtomicIntegerFieldUpdater

4.3 AtomicLongFieldUpdater

5. 原子累加器

专门用于进行累加的构造器,其效率要高于其他原子类自己的累加操作。

5.1 LongAdder

5.1.1 性能提升原因
  • 性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0累加Cell[0],而Thread-1累加Cell[1]…,最后将结果汇总。这样它们在累加时操作的不同的Cell变量,因此减少了CAS重试失败,从而提高性能。注意,Cell变量的个数一定是小于等于CPU核数的。
5.2.2 源码分析
  • 关键域

    //累加单元数组, 懒惰初始化
    transient volatile Cell[] cells;
    //基础值, 如果没有竞争, 则用cas累加这个域
    transient volatile long base;
    //在cells创建或扩容时, 置为1, 表示加锁
    transient volatile int cellsBusy;
    
  • CAS锁

    public class LockCas {
    	private AtomicInteger state = new AtomicInteger(0);
    	public void lock() {
    		while (true) {
    			if (state.compareAndSet(0, 1)) {
    				break;
    			}
    		}
    	}
    	public void unlock() {
    		log.debug("unlock...");
    		state.set(0);
    	}
    }
    
  • 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);
    	}
    	// 省略不重要代码
    }
    
  • add()方法
    在这里插入图片描述

    public void add(long x) {
    	// as 为累加单元数组
    	// b 为基础值
    	// x 为累加值
    	Cell[] as; long b, v; int m; Cell a;
    	// 进入 if 的两个条件
    	// 1. as 有值, 表示已经发生过竞争, 进入 if
    	// 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
    	if ((as = cells) != null || !casBase(b = base, b + x)) {
    		// uncontended 表示 cell 没有竞争
    		boolean uncontended = true;
    		if (
    		// as 还没有创建
    		as == null || (m = as.length - 1) < 0 ||
    		// 当前线程对应的 cell 还没有
    		(a = as[getProbe() & m]) == null ||
    		// cas 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell )
    		!(uncontended = a.cas(v = a.value, v + x))
    		) {
    			// 进入 cell 数组创建、cell 创建的流程
    			longAccumulate(x, null, uncontended);
    		}
    	}
    }
    

    1.如果第一个线程到来,此时不存在竞争,所以cells数组必定为null,此时进行casBase操作必定能成功,此时无需进入代码块。
    2.如果第一个线程进行caseBase操作时,第二个线程进入,此时cells依然为null,没有开始竞争,所以,第二个线程企图进行casBase操作,但是由于此时第一个线程正在进行该操作,所以第二个线程进入if的代码块内,此时cells仍然为null,执行longAccmulate方法。
    3.接下来,第三个线程到来,此时cells数组不为null,说明已经存在了竞争,因此,直接进入代码块,此时cells不为null,则判断当前线程是否存在一个对应的cells内的累加单元cell,如果存在了,直接进行累加,累加成功则直接返回,如果不成功则执行longAccmulate方法。如果不存在,直接执行longAccmulate方法。

  • longAccumulate()方法
    ① else if (cellsBusy == 0 && cells == as && casCellsBusy()) ==> 数组不存在时
    在这里插入图片描述
    ② if ((as = cells) != null && (n = as.length) > 0) ==> Cells数组存在但是当前线程对应的累加单元cell不存在时
    在这里插入图片描述
    ③ 接②成功的情况走下面的esle if
    在这里插入图片描述

    final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
    	int h;
    	// 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell
    	if ((h = getProbe()) == 0) {
    		// 初始化 probe
    		ThreadLocalRandom.current();
    		// h 对应新的 probe 值, 用来对应 cell
    		h = getProbe();
    		wasUncontended = true;
    	}
    	// collide 为 true 表示需要扩容
    	boolean collide = false; 
    	for (;;) {
    		Cell[] as; Cell a; int n; long v;
    		// 已经有了 cells
    		if ((as = cells) != null && (n = as.length) > 0) {
    			// 还没有 cell
    			if ((a = as[(n - 1) & h]) == null) {
    				// 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x
    				// 成功则 break, 否则继续 continue 循环
    			}
    			// 有竞争, 改变线程对应的 cell 来重试 cas
    			else if (!wasUncontended)
    				wasUncontended = true;
    			// cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null
    			else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
    				break;
    			// 如果 cells 长度已经超过了最大长度(cpu个数), 或者已经扩容, 改变线程对应的 cell 来重试 cas
    			else if (n >= NCPU || cells != as)
    				collide = false;
    			// 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了
    			else if (!collide)
    				collide = true;
    				// 加锁
    			else if (cellsBusy == 0 && casCellsBusy()) {
    				// 加锁成功, 扩容
    				continue;
    			}
    			// 改变线程对应的 cell,换个累加单元累加
    			h = advanceProbe(h);
    		}
    		// 还没有 cells, 尝试给 cellsBusy 加锁
    		else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
    			// 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell
    			// 成功则 break;
    		}
    		// 上两种情况失败, 尝试给 base 累加
    		else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
    			break;
    	}
    }
    
  • 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;
    }
    
5.3.3 原理解析

1.Cell的缓存伪共享

  1. 现代CPU的结构:现代CPU一般都采用三级缓存的结构,CPU从不同的位置中读取数据的速度是不同的,到寄存器大约需要的时钟周期为1cycle,到一级缓存需要3到4 cycle,到二级缓存需要10到20cycle,到三级缓存需要40到45cycle,到内存需要120~240cycle。因为CPU与内存的速度差异很大,所以需要靠预读数据至缓存来提升效率。
    在这里插入图片描述
  2. 缓存行:
    ① 缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64byte(8个long)。缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中。使用volatile关键字修饰数据后,CPU要保证数据的一致性,也就是说如果某个CPU核心更改了数据,其它CPU核心对应的整个缓存行必须失效,同时该CPU会将更新的数据同步到内存中,其他数据如果使用这些数据则必须要到内存中去取。
    ② 因为Cell是数组形式,在内存中是连续存储的(伪共享),一个Cell为24字节(16字节的对象头和8字节的 value),因此缓存行可以存下2个Cell对象。如下图所示,Core-0要修改Cell[0],Core-1要修改Cell[1]。无论谁修改成功,都会导致对方Core的缓存行失效。
    在这里插入图片描述
  3. @sun.misc.Contended用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,从而让CPU将对象预读至缓存时占用不同的缓存行,这样,不会造成更新Cell[0]时让对方Cell[1]缓存行的失效。
    在这里插入图片描述

5.2 LongAccumulator

三、Unsafe对象(了解)

  • 基本概念:Unsafe对象提供了非常底层的,操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值