一个线程安全问题的栗子
- 结果小于10w,和预期不符,线程不安全。
出现问题的原因
用javap查看编译结果
实质计算的过程就是压栈出栈的过程
根本问题所在是A线程还未写入主内存,B线程就读取到主内存的老数据
什么是原子操作
原子操作可以是一个步骤,也可以是多个步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
简单来说就是:整个操作视为一个整体,资源在该次操作中保持一致,操作期间不能有操作介入。
解决方案
方案一:用synchronized
方案二:用lock
方案三:用AtomicInteger
AtomicInteger原理一一CAS
CAS:即Comparea and swap
AtomicInteger就是用CAS+自旋(循环),自旋不是CAS的一部分,CAS原理就是compare and swap比较替换,自旋是CAS使用的一种思想。
这里A线程用CAS方式去读取主内存数据后进行压栈计算,如果期间B线程也用CAS方式去读取就会不断自旋等A线程把新值刷新到主内存后B线程再进去压栈计算。
CAS的底层实现
通过对象的引用和再内存地址中的偏移量。具体实现方法如unsafe.compareAndSwapInt()
- CAS同时具有 volatile 读和volatile写的内存语义,如:
A线程写volatile变量,随后B线程读这个volatile变量。
- 这里unsafe.compareAndSwapInt(Object var, long var2, int var3, int var4),其中var是CAS操作的对象本身,var2是预期值在主内存中内存地址的偏移量,var3是预期值的value,var4是修改值的value。
CAS就是提供原子操作的方式,JDK还提供了很多原子操作封装类
其他CAS原子操作类
AtomicIntegerArray的原理不是数组中每个元素都是AtomicInteger,而是普通的int数组。只是取出来的时候再进行CAS操作。
AtomicIntegerFieldUpdate是对某一个对象中int字段进行CAS操作。
AtomicReference原理是通过CAS来创建对象。
Adder的原理就是increment的累加sum。Accumlator可以自定义累加规则,类似stream中的reduce。
高并发的CAS可能会导致CPU性能消耗特别大,CPU飙升。所以1.8引入了Accumulator和Adder,思想是分而治之。
CAS的三个问题
1. CPU高负荷问题
循环+CAS,自旋的实现会让所有线程处于高频运行,争抢CPU执行时间的状态。如果长时间不成功会带来很大的CPU资源消耗。
2. 仅能对单个变量操作
仅针对单个变量的操作,不能对于多个变量来实现原子操作。
3. ABA问题
ABA问题
一个T1线程进行CAS比较的时候发现是相等的值,但此时的值已经不是以前的值了,而是被其他线程覆盖后再次变成T1线程比较相等的值了,此时T1线程执行更新就是无效的。可以通过使用带版本号的原子操作来解决。
带版本号的CAS
AtomicStampedReference就是带版本号的CAS操作类。
其实现原理是CAS时加上版本号,如:top.compareAndSet(oldTop, newTop, v, v+1)