前言
概述
与锁不同的是, CAS 是一种无锁操作,一种无阻塞的算法,它实质上不能说是一种锁,而是将 CPU 充分利用起来的一种算法
CAS 广泛应用在数据结构中,JDK中的 java.util.concurrent 并发包就是在其操作下建立的
众所周知,JAVA 作为一门高级语言,是不支持一些底层处理的,例如指针,内存控制等等,但大家可以看看 sun.misc.Unsafe 类,也是在它的支持下,JAVA 具备了对硬件级别原子操作的支持,这个包有很多应用,例如 java.util.concurrent.atomic 包下的原子类都是基于其实现 CAS 操作的
我的测试下,当线程数量不大时,CAS 要快于锁,但线程数量很多很多时,CAS 却更慢了
参考
http://blog.csdn.net/hsuxu/ar...
http://www.cnblogs.com/mickol...
CAS
概述
举个例子,如 i++,它是分三步的
先取内存中的 i
再将 i 加上 1
最后将加完后的值赋给内存中的 i
但若在其赋值前,i 的内存值已经被其他线程修改,此处肯定会丢失数据,也就是说它是线程不安全的
如果给这个操作加上锁,那代价未免也太大了,CAS 便可以更快地解决这个问题
原理
CAS 的原理其实很简单,主要分三个参数
内存值 - 内存里的实际值
旧期望值 - 操作前的值
新值 - 操作后的值
CAS 的操作简而言之就是 compare and swap
将内存值与旧期望值比较
若相等,则说明此值在操作中没有被其他线程改变过,并将新值赋给内存值
若不等,则说明此值在操作中已经被其他线程改变过,并一直自旋直到相等
ABA 问题
简而言之,ABA 问题就是,比如我取内存值 A,在我比较之前,它被其他人改成了 B,然后又被其他人改回了 A,而我之后再做比较,相等成立,但是又会造成数据丢失的问题
CAS 真正比较的应该是 值的状态,而不是值的大小,我可以给值附带一个 版本号,然后更新时对版本号进行值大小的 CAS 操作,或者附带一个 时间戳也是一样的,像现在数据库大部分都是采用附加版本号的方法
大家也可以看看 java.util.concurrent.atomic.AtomicStampedReference 是怎么解决 ABA 问题的,在这里就不讲述了
缺点
如果每个人都在自旋,CPU 的开销将是巨大的,关于本人的测试,当线程数量很多很多时,CAS 会更慢就是这个原因
AtomicInteger
我们来看看 java.util.concurrent.atomic.AtomicInteger 是怎么实现原子操作的,其主要成员如下
// Unsafe 类实例,具体的在下一篇文章详细讲,这里先跳过
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 值偏移量
private static final long valueOffset;
// 内部封装值
private volatile int value;
// unsafe 初始化
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
我们常用的 incrementAndGet 方法如下,在这里是直接调用 Unsafe 的 native 方法,实现硬件级别的原子操作,底层是用汇编实现的
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。这个本地方法的最终实现在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011openjdkhotspotsrcoscpuwindowsx86vm atomicwindowsx86.inline.hpp(对应于windows操作系统,X86处理器)。下面是对应于intel x86处理器的源代码的片段:
// 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
}
}
如上面源代码所示,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。
CPU
关于CPU的锁有如下3种:
处理器自动保证基本内存操作的原子性
首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性
使用总线锁保证原子性
第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2,如图
原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行加一操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。
处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
使用缓存锁保证原子性
第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。