Java多线程高并发进阶篇(三)-原子操作的实现原理
要研究原子操作,就必须要对原子操作的来龙去脉有个清晰的认识。我们从原子操作的概念,以及处理器的原子操作和Java中原子操作的实现说起。
一.原子操作的概念
我们在物理学中知道,原子是一个不可再分的最小粒子。同理,原子操作(atomic operation)就是指不可被中断的一个或者一系列操作。
先来了解几个基本概念。
二.处理器中如何实现原子操作
在<Java多线程高并发进阶篇(一)-volatile实现原理剖析>中,我们说到了总线锁和缓存锁的概念。没错,在多处理器中,就是使用总线锁和缓存锁来实现处理器之间的原子操作。
首先,我们要明确一点,处理器会自动保证基本的内存操作是原子的(要不然说处理器的内存操作有什么意义),也就是说当一个处理器访问内存中读取一个字节时,其他处理器是不能访问该字节的内存地址的(这是最基本的操作,要不然就乱套了)。
在最新型号的处理器中,都能自动保证处理器对同一个缓存行里的操作是原子的。
这些最基本操作的原子性,是处理器必须要实现的基本功能!
那么如何保证在复杂条件下的内存操作原子性(比如跨总线,跨多个缓存行,跨页表等)?
答案就是总线锁和缓存锁。我们再叙述一下这两东西。
1.总线锁
上一篇帖子中已经介绍过,我们复习一遍。
所谓总线锁,就是一个处理器对共享变量进行操作时,会在总线上声言一个LOCK#信号,当其他处理器对共享变量进行操作请求时,就会被阻塞,而该处理器就可以独占共享内存(任我行!)。
举个例子i=1,我们计算两次i++。我们预想的结果是3,但是,如果没有总线锁,是不是还能得到3?不一定,可能得到是2。
原因是多个处理器(CPU1,CPU2)同时读取自己缓存行中的i(缓存的都是1),去做操作i++,最后计算完成后都写回到主内存,那么你此时看到的就是2了。
所以,使用总线锁可以保证处理器之间的操作时原子性。
2.缓存锁(两个关键词:一个缓存行,缓存一致性协议)
我们在上一帖中也说到过,缓存锁是使用缓存一致性协议来保证处理器之间对缓存行的内存操作的原子性的。
普及一个基础知识,在内存中,频繁使用的数据会被缓存到处理器的一级,二级等高速缓存中。
所谓缓存锁定,就是在一个处理器中,对共享变量的操作不是在主内存中进行操作,而是在处理器自己的缓存中操作。当处理器要对该缓存行进行操作时,就把该缓存行对应的内存地址进行锁定。其他处理器要操作时,会使用嗅探技术进行探测,如果发现该地址被锁定了,那么它就会把自身存着该数据的缓存行设置为无效状态。这就保证了缓存一致性(数据始终保持一致),也就是缓存一致性协议能够阻止缓存了同一缓存行的处理器同时修改。
那什么情况下不能使用缓存锁?
一是当操作的数据根本就不能被缓存在处理器内部时候(废话),或者就是要操作的数据跨了多个缓存行(这当然,缓存锁只针对一个缓存行的数据),这时只能使用总线锁。
二是处理器不支持缓存锁。比如,Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
Intel处理器中,多个lock前缀指令就使用了上述两种机制实现。例如,位测试和修改令:BTS、BTR、BTC;交换指令XADD、CMPXCHG,以及其他一些操作数和逻辑指令(如ADD、OR)等
三.Java中如何实现原子操作?
在Java中,实现原子操作的方法大致有两种。一种是锁机制实现,另外一个就是使用无锁实现--CAS。
1.我们重点说一下无锁实现--CAS.
无锁机制,充分利用了处理器对于原子性的保证机制。CAS在处理器内部使用CMPXCHG完成原子性操作控制。在并发包下的原子实现类中,我们举一例。
在JDK中,Unsafe类的compareAndSwapInt方法的修饰符是native和final。也就是说,我们需要深入底层,看下这个本地方法的源码实现。这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp,最终实现在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp:
// Adding a lock prefix to an instruction on MP machine(添加一条lock前缀指令在MP机器上) // VC++ doesn't like the lock prefix to be on a single line // so we can't insert a label after the lock prefix. // By emitting a lock prefix, we can define a label after it. #define LOCK_IF_MP(mp) __asm cmp mp, 0 \ __asm je L0 \ __asm _emit 0xF0 \ __asm L0: inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // alternative for InterlockedCompareExchange int mp = os::is_MP(); __asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx } }
看不懂c语言代码没关系,我们看它的注释,大概能明白:
根据处理器的类型(是单处理器还是多处理器,MP是指Multiple,多个的意思),来决定是否加lock前缀指令.如果是单处理器,LOCK_IF_MP不成立,就不用在cmpxchg指令前加lock前缀指令(注释说VC++不喜欢在单行上加lock前缀指令,实际上单处理器可以自己维护访问一致性,还要lock指令干啥?).如果是多处理器,那么就在在cmpxchg指令前加lock前缀指令.
2.CAS中存在的问题
①著名的ABA问题
CAS在操作变量的时候。需要检测变量的值是否发生了变化.如果没有发现变化。它就会以为没有发生过更新操作。但是当我们把值从A-->B,然后B-->A时,其实是发生过变化的。那么如何解决这个问题?
很简单,给每次操作都加一个标志戳(stamp),那么上面的变化过程不是就成了1A->2B-->3A了。
在JDK1.5后,JDK的Atomic包里提供了一个类AtomicStampedReference,来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
②循环时间长
因为我们知道,在原子包中,实现CAS操作都使用了无限循环来进行自旋操作。那么肯定会造成CPU的开销大。
③只能保证一个共享变量的原子性操作。从上图我们也可以看到,基本的原子类只能实现对单个共享变量的修改,增加等过程。那么如何解决这个问题呢?从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。