CAS总结
n++的问题不能保证原子操作。 因为被编译后拆分成了3个指令,先获取值,然后加一,然后写回内存。
把变量声明为volatile,volatile只能保证内存可见性,但是不能保证原子性,在多线程并发下,无法保证线程安全。
三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,
否则什么都不做,并返回false。
CAS跟synchronized要达到的效果是一样的,保证同一个时刻只有一个线程可以操作成功。
只不过CAS是硬件级别上面的锁,会锁住总线,保证只有一个CPU可以访问内存,对于用户层面来说应该是乐观锁,synchronized是JVM级别的锁,开销比较大
分析一下 AtomicInteger,变量value存储的实际值,被volatile修饰,保证其值改变后,其他线程可以立马看见。
public class AtomicInteger extends Number implements java.io.Serializable { // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; public final int get() {return value;} } public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } // 并发累加操作 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; }
假设线程A和线程B同时执行getAndAdd操作(分别跑在不同CPU上):
假设AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据Java内存模型,线程A和线程B各自持有一份value的副本,值为3。
线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
线程B也通过getIntVolatile(var1, var2)方法获取到value值3,运气好,线程B没有被挂起,并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为2。
这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值(3)和内存的值(2)不一致,说明该值已经被其它线程提前修改过了,那只能重新来一遍了。
重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
在这里的一个前提是compareAndSwapInt总是能拿到内存的最新值,然后把自己手上的值跟内存最新值比较,如果相等,即符合预期,则更新,如果不一致,进行下一轮
循环,重新获取内存最新值。
底层是通过Unsafe这个类compareAndSwapInt进行比较替换,Unsafe类compareAndSwapInt这个是C++实现的,不同操作系统有不同的实现,它能直接操作的内存偏移地址,
拿到内存最新值,然后跟旧的值进行比较替换。它能在CPU级别实现锁的机制,保证原子性
大致原理就是
1.如果是多处理器,为cmpxchg指令添加lock前缀 ,单处理器,省略lock前缀。
单处理器实际上不存在多个线程同时并行操作的场景,因为只有一个cpu,操作都是串行的,CPU根据时间片轮询执行不同线程。
lock前缀可以保证后续执行指令的原子性,老的处理器的做法是锁住总线,开销很大,新的处理器使用缓存锁定。