Bootstrap

线程安全(二)java中的CAS机制

系列文章目录

线程安全(一)java对象头分析以及锁状态
线程安全(二)java中的CAS机制
线程安全(三)实现方法sychronized与ReentrantLock(阻塞同步)
线程安全(四)Java内存模型与volatile关键字
线程安全(五)线程状态和线程创建
线程安全(六)线程池
线程安全(七)ThreadLocal和java的四种引用
线程安全(八)Semaphore
线程安全(九)CyclicBarrier
线程安全(十)AQS(AbstractQueuedSynchronizer)

0.简介

CAS:Compare And Swap(比较并交换)
在java中,CAS机制是区别于synchronized的一种乐观锁机制。
synchronization是种独占锁,悲观锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
CAS机制是乐观锁(相对于synchronized),直接进行修改
CAS是自旋锁或者无锁,在此过程中运用了CPU的CAS的指令及JNI对底层操作修改变量。

1.synchronized关键字修饰

建两个示例模拟下一下多线程情况下如果对共享变量不加限制和使用synchronized关键字修饰之后的结果。

示例一

内存值(Memory)为 A,
线程1获取了Memory,
线程2也获取了Memory,
线程1想修改为C,线程2想修改为D;
对共享变量不加限制结果分两种:
1.线程1后运行完成,结果是C;
2.线程2后运行完成,结果是D;
结论:线程会同时执行,后运行线程的方法会决定最终值。

synchronized修饰的话:
1.线程1先获取锁,则先运行完成,结果是D;
2.线程2先获取锁,结果是C;
结论:线程会依(抢占)顺序执行,后运行线程的方法会决定最终值。
(结果不确定,但不会出现两个线程同时获取此变量进行操作。会会依(抢占)顺序操作。会加锁)

示例二

如果内存值(Memory)是1,
线程1获取了(Memory),值为1,
线程2也获取了(Memory),值为1,
线程1想加+1,线程2想+2;
对共享变量不加限制结果分三种:
1.线程1先运行,线程2在线程1运行完运行,结果是4;
2.线程2先运行,线程1在线程2运行完运行,结果是4;
3.同时获取,结果为3;

synchronized修饰的话:
最终结果是4。但在操作时会先锁定对象,会依次操作。
(结果确定,会加锁)

2.CAS机制在java中的出现

多线程情况下,共享变量风险很大,但锁机制又在运行效率上有很大问题,多线程时需要考虑安全和效率,jdk1.5之后,java.util.concurrent的出现简化了开发,他所应用的就是CAS机制,例如AtomicInteger类的自增操作等。
建两个示例说明一下多线程情况CAS使用情况。

示例一

内存值(Memory)为 A,
线程1获取了Memory,(旧的期望值)
线程2获取了Memory,(旧的期望值)
线程1想修改为C,(旧的期望值为Memory,新的期望值为C)
线程2想修改为D,(旧的期望值为Memory,新的期望值为D)
当线程1想把Memory从A改为C时,会验证一下此时的Memory是否依然是A,不是的话,什么都不做。(CAS的原理)
同理,线程2也是如此操作。
外层加入循环do{}while(),判断条件为,当就得期望值与现在获取的内存值不一致时,再重新获取。(当比较并替换操作失败时,重新获取下Memory,直到比较并替换成功)。

示例二

内存值(Memory)是1,
线程1获取了(Memory),(旧的期望值),
线程2也获取了(Memory),(旧的期望值),
线程1想加+1,线程2想+2;
当线程1想把Memory从+1时,会验证一下此时的Memory是否依然是1,不是的话,什么都不做。(CAS的原理)
同理,线程2也是如此操作。
外层加入循环,当上述更改操作失败时,重新获取下Memory,直到更改成功。
外层加入循环do{}while(),判断条件为,当就得期望值与现在获取的内存值不一致时,再重新获取,(当比较并替换操作失败时,重新获取下Memory,直到比较并替换成功)。
最终结果一定是4(固定值,无锁)。

3.CAS原理与实现

CAS原理
CAS(Compare and Swap):比较并交换。通过利用底层硬件平台的特性,实现原子性操作。
CAS实现
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS调用层次
java:Unsafe类
Native:CompareAndSwapInt
atomic_linux_x86
asm LOCK_IF_MP( Multi Processor)(多核lock)
lock cmpxchg 指令
硬件:
lock指令在执行后面指令的时候锁定一个北桥信号
(不采用锁总线的方式)

在java中,CAS通过调用JNI的代码实现的。
(JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。)
而compareAndSwapInt就是借助C来调用CPU底层指令实现的。
程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。
如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。
反之,如果程序是在单处理器上运行,就省略lock前缀
(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

CAS的缺点:

 1. 问题: A-B-A模式。一个值原来为A,先变为B,再变为A,那么使用CAS进行检查时会无法区分这个值有没有变化。
 	解决办法: 1A-2B-3A,通过一个前置的版本号。JAVA1.5开始,JDK的atomic包中提供了AtomicStampedReference类(附时间戳)。
 他的compareAndSet()方法,比较两个方面 当前引用等于预期引用,当前标志等于预期标志。有点类似于数据库乐观锁的版本号。
 2. 循环时间长,开销大。
 3. 问题: 只能保证一个共享变量的原子操作。
	解决办法:从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,
你可以把多个变量放在一个对象里来进行CAS操作。

4.硬件层面的实现

CPU锁分为三种

  • 处理器自动保证基本内存操作的原子性(简单内存操作)
  • 使用总线锁保证原子性(复杂内存操作)
  • 使用缓存锁保证原子性(复杂内存操作)

后两种机制可以使用LOCK前缀的指令实现。

intel的手册对lock前缀的说明如下

  • 确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
  • 禁止该指令与之前和之后的读和写指令重排序。
  • 把写缓冲区中的所有数据刷新到内存中。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;