根据修改的数据类型,可以将JUC包中的原子操作类可以分为4类。
1.基本类型
: AtomicInteger, AtomicLong, AtomicBoolean ;
2.数组类型
: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
3.引用类型:
AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
4.对象的属性修改类型
: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。
这些类存在的目的是对相应的数据进行原子操作。
原子操作的描述是:多个线程执行一个操作时,其中任何一个线程要么完全执行完此操作,要么没有执行此操作的任何步骤,那么这个操作就是原子的。
这些类主要都是使用CAS机制以及volatile关键字来保证操作原子性
AtomicInteger, AtomicLong和AtomicBoolean
这3个基本类型的原子类的原理和用法相似。以下以AtomicLong为例对基本类型的原子类进行介绍
AtomicLong是作用是对长整形进行原子操作。在32位操作系统中,64位的long和double变量由于会被JVM当作两个分离的32位来进行操作,所以不具有原子性。而使用AtomicLong能让long的操作保持原子型。
AtomicLong函数列表
1 | // 构造函数 |
AtomicLongArray
AtomicLongArray的作用是对”长整形数组”进行原子操作。
1 | // 创建给定长度的新AtomicLongArray。 |
AtomicReference
AtomicReference是作用是对”对象”进行原子操作。
1 | // volatile类型 |
V是泛型,将对象使用AtomicReference包装在进行修改将具有原子性
1 | Object obj = new Object(); |
AtomicLongFieldUpdater
AtomicLongFieldUpdater可以对指定”类的’volatile long’类型的成员”进行原子更新。它是基于反射原理实现的。
函数列表
1 | // 受保护的无操作构造方法,供子类使用。 |
AtomicLongFieldUpdater示例
1 | // LongTest.java的源码 |
newUpdater()方法源代码分析
1 | public static <U> AtomicLongFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName) { |
该方法主要返回一个AtomicLongFieldUpdater类型的对象,用于设置对象中的long型attribute。
他返回CASUpdater还是LockedUpdater取决于JVM是否支持long型的CAS函数,这两个类都是AtomicLongFieldUpdater的子类。
从上述源代码我们可以知道,在JUC的原子类操作中,主要会使用CAS以及volatile关键字来保证操作的原子性。
指令重排序
Java语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程通过叫做指令的重排序。指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。
程序执行最简单的模型是按照指令出现的顺序执行,这样就与执行指令的CPU无关,最大限度的保证了指令的可移植性。这个模型的专业术语叫做顺序化一致性模型。但是现代计算机体系和处理器架构都不保证这一点(因为人为的指定并不能总是保证符合CPU处理的特性)。
在两个线程交替执行的情况下数据的结果就不确定了,在机器压力大,多核CPU并发执行的情况下,数据的结果将更加不确定。
Happens-before法则
Java存储模型有一个happens-before原则,就是如果动作B要看到动作A的执行结果(无论A/B是否在同一个线程里面执行),那么A/B就需要满足happens-before关系。
在介绍happens-before法则之前介绍一个概念:JMM动作(Java Memeory Model Action),Java存储模型动作。一个动作(Action)包括:变量的读写、监视器加锁和释放锁、线程的start()和join()。
happens-before完整规则:
- 同一个线程中的每个Action都happens-before于出现在其后的任何一个Action。
- 对一个监视器的解锁happens-before于每一个后续对同一个监视器的加锁。
- 对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作。
- Thread.start()的调用会happens-before于启动线程里面的动作。
- Thread中的所有动作都happens-before于其他线程检查到此线程结束或者Thread.join()中返回或者Thread.isAlive()==false。
- 一个线程A调用另一个另一个线程B的interrupt()都happens-before于线程A发现B被A中断(B抛出异常或者A检测到B的isInterrupted()或者interrupted())。
- 一个对象构造函数的结束happens-before与该对象的finalizer的开始
- 如果A动作happens-before于B动作,而B动作happens-before与C动作,那么A动作happens-before于C动作。
CAS操作
所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
乐观锁用到的机制主要就有CAS,Compare and Swap。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
非阻塞算法 (nonblocking algorithms)
一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而compareAndSet() 就用这些代替了锁定。
拿出AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的。
1 | private volatile int value; |
整体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。
而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。参考资料的文章中介绍了如果利用CAS构建非阻塞计数器、队列等数据结构。
CAS看起来很爽,但是会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。
一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。这允许一对变化的元素进行原子操作。