什么是原子操作
原子操作:一个或多个操作在CPU执行过程中不被中断的特性
当我们说原子操作时,需要分清楚针对的是CPU指令级别还是高级语言级别。
比如:经典的银行转账场景,是语言级别的原子操作;
而当我们说volatile修饰的变量的复合操作,其原子性不能被保证,指的是CPU指令级别。
二者的本质是一致的。
“原子操作”的实质其实并不是指“不可分割”,这只是外在表现,本质在于多个资源之间有一致性的要求,操作的中间态对外不可见。
比如:在32位机器上写64位的long变量有中间状态(只写了64位中的32位);银行转账操作中也有中间状态(A向B转账,A扣钱了,B还没来得及加钱)
原子操作类
从java1.5开始,jdk提供了java.util.concurrent.atomic包,这个包中的原子操作类,提供了一种用法简单,性能高效,线程安全的更新一个变量的方式。
atomic包里面一共提供了13个类,分为4种类型,分别是:原子更新基本类型,原子更新数组,原子更新引用,原子更新属性,这13个类都是使用Unsafe实现的包装类。
1)原子操作基本类型
atomic提供了3个类用于原子更新基本类型:分别是AtomicInteger原子更新整形,AtomicLong原子更新长整形,AtomicBoolean原子更新bool值。由于这三个类提供的方法几乎是一样的,因此本节以AtomicInteger为例进行说明。
AtomicInteger的常用方法有:
1.int addAndGet(int delta):以原子的方式将输入的值与实例中的值相加,并把结果返回
2.boolean compareAndSet(int expect, int update):如果输入值等于预期值,以原子的方式将该值设置为输入的值
3.final int getAndIncrement():以原子的方式将当前值加1,并返回加1之前的值
4.void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
5.int getAndSet(int newValue):以原子的方式将当前值设置为newValue,并返回设置之前的旧值
AtomicInteger使用示例,如下图
2)原子操作数组类
atomic里提供了三个类用于原子更新数组里面的元素,分别是:
AtomicIntegerArray:原子更新整形数组里的元素
AtomicLongArray:原子更新长整形数组里的元素
AtomicReferenceArray:原子更新引用数组里的元素
因为每个类里面提供的方法都一致,因此以AtomicIntegerArray为例来说明。AtomicIntegerArray主要提供了以原子方式更新数组里的整数,常见方法如下:
int addAndGet(int i, int delta):以原子的方式将输入值与数组中索引为i的元素相加
boolean compareAndSet(int i, int expect, int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
3)原子操作引用类
原子更新基本类型的AtomicInteger只能更新一个变量,如果要原子更新多个变量,就需要使用原子更新引用类型提供的类了。原子引用类型atomic包主要提供了以下几个类:
AtomicReference:原子更新引用类型
AtomicReferenceFieldUpdater:原子更新引用类型里的字段
AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)
4)原子操作属性类
如果需要原子更新某个对象的某个字段,就需要使用原子更新属性的相关类,atomic中提供了一下几个类用于原子更新属性:
AtomicIntegerFieldUpdater:原子更新整形属性的更新器
AtomicLongFieldUpdater:原子更新长整形的更新器
AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
想要原子的更新字段,需要两个步骤:
1、因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性
2、更新类的字段(属性)必须使用public volatile修饰符
java中原子操作的实现方式
1)用原子操作类实现原子操作(原子操作类内部是使用CAS实现原子操作的)
会有ABA问题
根源:CAS的本质是对变量的current value ,期望值 expected value 进行比较,二者相等时,再将 给定值 given update value 设为当前值。
因此会存在一种场景,变量值原来是A,变成了B,又变成了A,使用CAS检查时会发现值并未变化,实际上是变化了。
对于数值类型的变量,比如int,这种问题关系不大,但对于引用类型,则会产生很大影响。
ABA问题解决思路:版本号。在变量前加版本号,每次变量更新时将版本号加1,A -> B -> A,就变成 1A -> 2B -> 3A。
JDK5之后Atomic包中提供了AtomicStampedReference#compareAndSet来解决ABA问题。
CAS只能对单个共享变量如是操作,对多个共享变量操作时则无法保证原子性,此时可以用锁。
另外,也可“取巧”,将多个共享变量合成一个共享变量来操作。比如a=2,b=t,合并起来ab=2t,然后用CAS操作ab.
JDK5提供AtomicReference保证引用对象间的原子性,它可将多个变量放在一个对象中来进行CAS操作。
2)使用锁实现原子操作
锁机制保证只有拿到锁的线程才能操作锁定的内存区域。
JVM内部实现了多种锁,偏向锁、轻量锁、互斥锁。不过轻量锁、互斥锁(即不包括偏向锁),实现锁时还是使用了CAS,即:一个线程进入同步代码时用自CAS拿锁,退出块的时候用CAS释放锁。
synchronized锁定的临界区代码对共享变量的操作是原子操作。
CPU如何实现原子操作
首先,CPU会自动保证基本的内存操作的原子性。CPU保证从内存中读写一个字节是原子的,即:当一个CPU读一个字节时,其他处理器不能访问这个字节的内存地址。
但对于复杂的内存操作如跨总线跨度、跨多个缓存行的访问,CPU是不能自动保证的。不过,CPU提供总线锁定和缓存锁定。
1)使用总线锁保证原子性
假如多个处理器同时读改写共享变量,这种操作(e.g. i++)不是原子的,操作完的共享变量的值会和期望的不一致。
原因:多个处理器同时从各自缓存读i,分别 + 1,分别写入内存。要想保证读改写共享变量的原子性,必须保证CPU1读改写该变量时,CPU2不能操作缓存了该变量内存地址的缓存。
总线锁就是解决此问题的。
总线锁:利用LOCK#信号,当一个CPU在总线上输出此信号,其他CPU的请求会被阻塞,则该CPU可以独占共享内存。
2)使用缓存锁保证原子性
同一时刻,其实只要保证对某个内存地址的操作是原子的即可,但总线锁定把CPU和内存间的通信锁住了。锁定期间,其他CPU不能操作其他内存地址的数据,所以总线锁定的开销比较大。目前CPU会在一些场景下使用缓存锁替代总线锁来优化。
频繁使用的内存会被缓存到L1、L2、L3高速cache中,原子操作可直接在高速cache中进行,不需要声明总线锁。
缓存锁是指:缓存一致性机制阻止同时修改由两个以上CPU缓存的内存区域数据,当其他CPU回写已被锁定的缓存行数据时,会使缓存行无效。
使用原子操作的好处
1)性能角度:它执行多次的所消耗的时间远远小于由于线程所挂起到恢复所消耗的时间,因此无锁的CAS操作在性能上要比同步锁高很多;
2)业务需求:业务本身的需求上,无锁机制本身就可以满足我们绝不多数的需求,并且在性能上也可以大大的进行提升。
例子:我们使用的版本控制工具与之其实非常的相似,如果使用锁来同步,其实就意味着只能同时一个人对该文件进行修改,
此时其他人就无法操作文件,如果生活中真正遇到这样的情况我们一定会觉得非常不方便,
而现实中我们其实并不是这样,我们大家都可以修改这个文件,只是谁提交的早,那么他就把他的代码成功提交的版本控制服务器上,
其实这一步就对应着一个原子操作,而后操作的人往往却因为冲突而导致提交失败,此时他必须重新更新代码进行再次修改,重新提交。