什么是原子操作?
假如有俩个操作a和b,如果从执行a的线程来看,当另一个线程执行b的时候,要么将
b全部执行完,要不完全不执行b,那么a和b对于彼此来说就是原子的。
如何实现原子操作
实现原子操作的话,我们可以通过使用锁,锁机制来满足基本需求是没问题了,但是我们有时候需要更有效,更灵活的机制。我们用的synchronized关键字是基于阻塞的锁机制,就是当一个线程拥有锁的时候,访问同一资源的其他线程需要等待,直到锁资源被释放。
这样会出现一些问题:如果获得锁的线程一直不释放锁资源,或者有大量线程竞争资源,那cpu就会花费大量时间和资源来处理,可能还会出现死锁,对于这种比较粗糙,粒度比较大的机制,明显不太适合我们。
实现原子操作可以使用当前处理器基本都支持CAS()的指令。每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望值A和一个新值B,操作的时候如果这个内存地址上存放的值等于这个期望的值,就把地址上的值赋给新值B,否则不做任何操作。但是要返回原值是多少。循环CAS就是一个循环不断的做CAS操作(自旋),直到成功为止。
CAS如何实现线程安全:语言层面不做处理,利用cpu的多处理能力,实现硬件层面的阻塞,再加上volatile变量的特性来实现。
CAS实现原子操作的三大问题
- ABA问题
在CAS操作值得时候,检查值有没有变化,如果没有发生变化则更新,但是如果这个值原来是A,然后变成了B,然后又变成了A,那么使用CAS进行检查时会发现这个值没有发生变化,但实际上是变化了,这就很恶心了。
这就好像:你接了杯生命源泉放桌子上,然后被老板叫去,然后有个人过来喝了半杯,他又给你接满,你回来得时候水看着没变化,但其实是变化过了。(就问你恶心吗???)
解决办法:使用版本号,当你接好水后,版本号为1,那个人过来喝了半杯版本告是2,又给你接满就是3。 1A------2B------3A,有这么一个变量来储存这个版本号。
- 开销大
CAS操作当中,当那个内存地址值和期望值比较得时候,如果不相等,那么它会一直自旋,如果长时间不成功,会给cpu带来非常大得开销。 - 只能保证一个共享变量的原子操作
当对一个共享变量执行操作得时候,我们可以通过循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性了。这个时候就可以用锁。
还有一个办法,就是把多个共享变量合并成一个共享变量来操作。
JDK中相关原子操作类的使用
我们就说一些平时用到概率大的类
更新基本类型
-
AtomicInteger
int addAndGet(int delta) :输入的值和实例中的值(初始化的值)以原子方式相加返回结果
boolean compareAndSet(int expect,int update):输入的值与预期值相等,则以原子方式将该值设置成输入值
int getAndIncrement():以原子方式给当前值加1,但是返回的是自增前的值
int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值 -
AtomicIntegerArray
int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加
boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值
注意:数组value通过构造方法传递进去,然后AtomicIntegerArray会把前数组复制一份,所以当它对内部数组元素进行修改并不会影响传入的数组
更新引用类型
- AtomicReference
原子更新引用类型 - AtomicStampedReference
AtomicStampedReference 是使用 pair 的 int stamp 作为计数器使用,利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在 ABA 问题了 - AtomicMarkableReference
原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引 用类型。构造方法是 AtomicMarkableReference(V initialRef,booleaninitialMark)
AtomicStampedReference和AtomicMarkableReference他俩就像好基友,都可以解决ABA问题,但是侧重点不同,AtomicStampedReference侧重更新的次数,而AtomicMarkableReference侧重是否被人动过。
更新字段类型
- AtomicIntegerFieldUpdater
原子更新整型的字段的更新器 - AtomicLongFieldUpdater
原子更新长整型字段的更新器 - AtomicReferenceFieldUpdater
原子更新引用类型里的字段
原子更新字段类需要俩步:1.因为更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,然后去设置想要更新的类和属性。2.更新类的属性必须使用public volatile修饰符。