概念
原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。原子性就是指该操作是不可再分的。
如果一个操作是原子性的,那么多线程并发的情况下,就不会出现变量被修改的情况。
比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么说这个操作是原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。
非原子操作都会存在线程安全问题,需要使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么称它具有原子性。java的concurrent包下提供了一些原子类,比如:AtomicInteger、AtomicLong、AtomicReference等。
由Java内存模型来直接保证的原子性变量操作包括read、load、use、assign、store和write六个,大致可以认为基础数据类型的访问和读写是具备原子性的。
如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock与unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐匿地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块—synchronized关键字,因此在synchronized块之间的操作也具备原子性。
Java中的原子操作
变量操作
- 除了long和double 之外的基本数据类型(int byte boolean short char float)的赋值操作。
- 所有引用类型的reference的赋值操作,无论是32位还是64位机器。
java对long和double的赋值操作是非原子操作!!long和double占用的字节数都是8,也就是64bits。
在32位操作系统上对64位的数据的读写要分两步完成,每一步取32位数据。这样对double和long的赋值操作就会有问题:如果有两个线程同时写一个变量内存,一个进程写低32位,而另一个写高32位,这样将导致获取的64位数据是失效的数据。因此需要使用volatile关键字来防止此类现象。
volatile本身不保证获取和设置操作的原子性,仅仅保持修改的可见性。但是java的内存模型保证声明为volatile的long和double变量的get和set操作是原子的。
Atomic类
java.concurrent.Atomic.*包中的所有类的原子操作。atomic包里面一共提供了13个类,分为4种类型,分别是:原子更新基本类型,原子更新数组,原子更新引用,原子更新属性,这13个类都是使用Unsafe实现的包装类。
用原子操作类实现原子操作(原子操作类内部是使用CAS实现原子操作的)会有ABA问题。
ABA问题解决思路:版本号。在变量前加版本号,每次变量更新时将版本号加1,A -> B -> A,就变成 1A -> 2B -> 3A。
JDK5之后Atomic包中提供了AtomicStampedReference#compareAndSet来解决ABA问题。
CAS锁优点
- 性能角度:它执行多次的所消耗的时间远远小于由于线程所挂起到恢复所消耗的时间,因此无锁的CAS操作在性能上要比同步锁高很多;
- 业务需求:业务本身的需求上,无锁机制本身就可以满足我们绝大多数的需求,并且在性能上也可以大大的进行提升。
锁机制
锁机制保证只有拿到锁的线程才能操作锁定的内存区域。synchronized锁定的临界区代码对共享变量的操作是原子操作。
JVM内部实现了多种锁,偏向锁、轻量锁、互斥锁。不过轻量锁、互斥锁(即不包括偏向锁),实现锁时还是使用了CAS,即:一个线程进入同步代码时用自CAS拿锁,退出块的时候用CAS释放锁。
CPU的原子操作
CPU会自动保证基本的内存操作的原子性。CPU保证从内存中读写一个字节是原子的。即:当一个CPU读一个字节时,其他处理器不能访问这个字节的内存地址。对于复杂的内存操作如跨总线跨度、跨多个缓存行的访问,CPU是不能自动保证的。不过,CPU提供总线锁定和缓存锁定。
总线锁
利用LOCK#信号,当一个CPU在总线上输出此信号,其他CPU的请求会被阻塞,则该CPU可以独占共享内存。
假如多个处理器同时读改写共享变量,这种操作(i++)不是原子的,操作完的共享变量的值会和期望的不一致。
原因:多个处理器同时从各自缓存读i,分别 + 1,分别写入内存。要想保证读改写共享变量的原子性,必须保证CPU1读改写该变量时,CPU2不能操作缓存了该变量内存地址的缓存。
总线锁就是解决此问题的。
缓存锁(缓存一致性)
缓存一致性机制阻止同时修改由两个以上CPU缓存的内存区域数据,当其他CPU回写已被锁定的缓存行数据时,会使缓存行无效。
同一时刻,其实只要保证对某个内存地址的操作是原子的即可,但总线锁定把CPU和内存间的通信锁住了。锁定期间,其他CPU不能操作其他内存地址的数据,所以总线锁定的开销比较大。目前CPU会在一些场景下使用缓存锁替代总线锁来优化。
频繁使用的内存会被缓存到L1、L2、L3高速cache中,原子操作可直接在高速cache中进行,不需要声明总线锁。