术语
在上一文关于synchronized锁机制的探寻中,我们知道在那些锁机制的底层实现中或多或少的都借助了CAS操作,其实Java中java.util.concurrent包的实现也是差不多建立在CAS之上,可见CAS在Java同步领域的重要性。
CAS是Compare and Swap的简写形式,可翻译为:比较并交换。用于在硬件层面上提供原子性操作。其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用。比较是否和给定的数值一致,如果一致则修改,不一致则不修改。
CAS案例分析
AtomicInteger的原子特性就是CAS机制的典型使用场景。 其相关的源码片段如下(以下代码基于JDK1.7 以及openJDK7):
private volatile int value; public final int get() { return value; } public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } } public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
AtomicInteger在没有锁的机制下借助volatile原语,保证了线程间的数据是可见的(共享的)。是get()方法可以获取最新的内存中的值。
在++1的操作中,使用了CAS操作,每次从内存中读取最新的数据然后将此数据+1,最终写入内存时,先比较内存中最新的值,同累加之前读出来的值是否一致,不一致则写失败,循环重试直到成功为止。
compareAndSet的具体实现调用了 unsafe类的compareAndSwapInt方法,它其实是一个Java Native Interface(简称JNI)java本地方法,会根据不同的JDK环境调用不同平台的对应C实现,下面以windows操作系统,X86处理器的实现为例,这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomic_windows_x86.inline.hpp,它的实现代码存在于:openjdk7\hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp,下面是相关的代码片段:
// Adding a lock prefix to an instruction on MP machine // 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 } }
由上面源代码可见在该平台的处理器上CAS通过指令cmpxchg(就是x86的比较并交换指令)实现,并且程序会根据当前处理器是否是多处理器(is_MP)来决定是否为cmpxchg指令添加lock前缀(LOCK_IF_MP),如果是单核处理器则省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果(而在JDK9中,已经忽略了这种判断都会直接添加lock前缀,这或许是因为现代单核处理器几乎已经消亡)。关于Lock前缀指令:
1. Lock前缀指令可以通过对总线或者处理器内部缓存加锁,使得其他处理器无法读写该指令要访问的内存区域,因此能保存指令执行的原子性。
2. Locl前缀指令将禁止该指令与之前和之后的读和写指令重排序。
3. Lock前缀指令将会把写缓冲区中的所有数据立即刷新到主内存中。
通过以上分析,我们深入CAS内部实现结合具体平台实现,就知道了CAS到底是如何保证操作的原子性了。虽然在X86平台上的实现Lock前缀指令会将禁止指令重排序和将数据立即刷新到主存,但是依然不能认为所有的CAS操作都具有有序性和可见性,只有当操作的变量本身是被volatile修饰的时候,我们才能说这样的CAS操作既有原子性也有有序性和可见性。单纯的CAS操作依然只是提供原子性。
2、缓存锁定是改进后的方案。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定
CAS缺陷
1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
4. 总线风暴带来的本地延迟。在上一章偏向锁的介绍中,我们提到CAS指令存在本地延迟,那么到底是指什么呢?我们知道多处理架构中,所有处理器会共享一条总线,靠此总线连接主存,每个处理器核心都有自己的高速缓存,各核相对于BUS对称分布,这种结构称为“对称多处理器”即SMP。当主存中的数据同时存在于多个处理器高速缓存的时候,某一个处理器的高速缓存中相应的数据更新之后,会通过总线使其它处理器的高速缓存中相应的数据失效,从而使其重新通过总线从主存中加载最新的数据,大家通过总线的来回通信称为“Cache一致性流量”,因为总线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个核心CAS成功时必然会引起总线风暴,这就是所谓的本地延迟。而偏向锁就是为了消除CAS,降低Cache一致性流量。
NUMA(Non Uniform Memory Access Achitecture)架构:
与SMP对应还有非对称多处理器架构,现在主要应用在一些高端处理器上,主要特点是没有总线,没有公用主存,每个Core有自己的内存
相关推荐
Java内存模型,简称JMM(Java Memory Model),是Java编程语言规范的一部分,它定义了线程如何共享和访问内存,以及在并发编程中如何处理数据一致性的问题。理解JMM对于编写高效、线程安全的Java代码至关重要。 1. ...
Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一个抽象概念,它规定了程序中各个线程如何访问共享变量,以及对这些访问进行同步控制的规则。理解Java内存模型对于编写多线程并发程序至关...
5. **JMM(Java Memory Model)详解**:详细介绍Java内存模型的规范,包括数据同步、 Happens-Before原则,以及它们如何确保多线程环境下的正确性。 6. **编译器优化与内存模型**:探讨JIT(Just-In-Time)编译器的...
Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一个抽象概念,它描述了在多线程环境下,如何在共享内存中读写变量,以及这些读写操作的可见性、原子性和有序性。这个模型规定了线程与主内存...
总的来说,Java的线程安全内存模型通过各种同步机制(如volatile、synchronized和CAS)来确保多线程环境下的数据一致性。开发人员需要根据具体的应用场景选择合适的同步策略,以平衡线程安全与程序性能。
4. **Java内存模型(JMM)**:JMM定义了线程如何访问共享变量,以及在多个线程之间如何同步这些变量。它规定了volatile变量的读写规则、synchronized的内存语义以及 Happens-Before原则等,以确保正确性并避免数据...
在深入理解Java内存模型(JMM)及并发三大特性方面,我们需要先建立对多线程、共享内存模型、可见性、有序性和原子性的基础概念。Java内存模型是Java并发编程的核心,它定义了共享变量在多线程环境中的行为规则和...
Java多线程、锁机制和内存模型是Java编程中至关重要的一部分,尤其在面试时,这些都是考察候选人技术深度和广度的常见话题。以下是对这些关键概念的详细解释: 1. **Java多线程**:Java提供了多种方式创建线程,如...
Java 内存模型(Java Memory Model,JMM)是 Java 语言规范中定义的一种内存模型,描述了 Java 虚拟机(JVM)如何访问和操作内存中的变量。JMM 包括变量的可见性、原子性和有序性等概念。 2. 并发编程基础 并发...
本文将深入探讨其中的关键概念,如Java内存模型(JMM)、线程通信机制、内存共享以及相关的同步机制。 首先,Java内存模型(JMM)是理解和解决并发问题的基础。JMM规定了线程如何访问和修改共享数据,以确保多线程...
7. **JVM内存模型**:理解Java内存模型(JMM)对于编写高性能的并发代码至关重要。它规定了线程如何访问和修改共享变量,以及何时能观察到其他线程对变量的修改。 通过学习“实战Java高并发程序设计”,开发者将...
13. Java内存模型(JMM):理解主内存、工作内存以及它们之间的交互规则。 14. volatile和synchronized在JMM中的作用:分析这两个关键字如何确保可见性和有序性。 六、并发设计模式 15. 生产者消费者模式:通过阻塞...
"04 并发编程专题06.zip"这个压缩包文件包含了两个部分:"JMM&volatile详解(下)(1).vep"和"JMM&volatile详解(下)(2).vep",它们着重探讨了Java内存模型(JMM)以及volatile关键字的深入理解。 Java内存模型...
2. **Java内存模型**:Java内存模型(JMM)是理解并发性能和正确性的关键。书中有详细讲解JMM如何确保线程间的可见性、有序性和原子性,以及volatile、synchronized和final关键字的作用。 3. **线程同步**:书中...
5. JAVA内存模型基础知识:JMM内存模型、顺序一致性、指令重排序、happens-before原则、as-if-serial、final内存语义、线程可见性、synchronized、volatile等。 6. 线程池基础知识:CachedThreadPool、...
总的来说,Java的高并发核心源码涉及到线程管理、同步机制、并发数据结构、内存模型以及异步编程模型等多个方面。理解并熟练运用这些技术,能有效提升Java程序在高并发环境下的性能和稳定性。开发者需要不断学习和...
Java内存模型(JMM)规定了线程如何访问和更新共享内存,以及这些操作的可见性。理解JMM有助于避免并发编程中的数据不一致性。 此外,`java.util.concurrent.atomic`包中的原子类提供了在无同步的情况下实现线程...
接着,深入理解Java内存模型(JMM)和volatile关键字至关重要。JMM规定了线程如何访问共享变量,确保多线程环境下的数据一致性。volatile确保了变量对所有线程的可见性,避免了数据的不一致性和内存可见性问题。 ...