文章目录
参考文章
java Unsafe类中compareAndSwap相关介绍
AtomicInteger源码分析——基于CAS的乐观锁实现
AtomicInteger源码分析
1.乐观锁和悲观锁
首先我们要明白一个知识点,乐观锁和悲观锁到底是什么
1.1 悲观锁
1.2.1 概念
cpu是时分复用的,也就是把cpu的时间片,分配给不同的thread/process轮流执行,时间片与时间片之间,需要进行cpu切换,也就是会发生进程的切换。切换涉及到清空寄存器,缓存数据。然后重新加载新的thread所需数据。当一个线程被挂起时,加入到阻塞队列,在一定的时间或条件下,在通过notify(),notifyAll()唤醒回来。在某个资源不可用的时候,就将cpu让出,把当前等待线程切换为阻塞状态。等到资源(比如一个共享数据)可用了,那么就将线程唤醒,让他进入runnable状态等待cpu调度。这就是典型的悲观锁的实现。独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
1.2.2 悲观锁的缺点
在进程挂起和恢复执行过程中存在着很大的开销。当一个线程正在等待锁时,它不能做任何事,所以悲观锁有很大的缺点。
例子:如果一个线程需要某个资源,但是这个资源的占用时间很短,当线程第一次抢占这个资源时,可能这个资源被占用,如果此时挂起这个线程,可能立刻就发现资源可用,然后又需要花费很 的长的时间重新抢占锁,时间代价就会非常的高。
1.2 乐观锁
每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。在上面的例子中,某个线程可以不让出cpu,而是一直while循环,如果失败就重试,直到成功为止。所以,当数据争用不严重时,乐观锁效果更好。比如CAS就是一种乐观锁思想的应用。
2.CAS(Compare and Swap)
2.1 概念
CAS就是Compare and Swap的意思,比较并操作。很多的cpu直接支持CAS指令。CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
2.2 JDK中的CAS(JUC)
是通过compareAndSwapXXX方法实现的
/**
* 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
*
* @param obj 需要更新的对象
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值
* @return 如果field的值被更改返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
JDK1.5中引入了底层的支持,在int、long和对象的引用等类型上都公开了CAS的操作,并且JVM把它们编译为底层硬件提供的最有效的方法,在运行CAS的平台上,运行时把它们编译为相应的机器指令。在java.util.concurrent.atomic包下面的所有的原子变量类型中,比如AtomicInteger,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操作。
2.2.1CAS操作中的ABA问题
如果V的值先由A变成B,再由B变成A,那么仍然认为是发生了变化,并需要重新执行算法中的步骤
解决方案:
不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号,即使这个值由A变为B,然后为变为A,版本号也是不同的。
AtomicStampedReference和AtomicMarkableReference支持在两个变量上执行原子的条件更新。AtomicStampedReference更新一个“对象-引用”二元组,通过在引用上加上“版本号”,从而避免ABA问题,Atomic MarkableReference将更新一个“对象引用-布尔值”的二元组。
2.3 AtomicInteger的实现
AtomicInteger 是一个支持原子操作的 Integer 类,就是保证对AtomicInteger类型变量的增加和减少操作是原子性的,不会出现多个线程下的数据不一致问题。如果不使用AtomicInteger,要实现一个按顺序获取的ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的 ID 的现象
接下来通过源代码来看AtomicInteger具体是如何实现的原子操作
首先看incrementAndGet() 方法,下面是具体的代码。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
getAndAddInt()方法调用的compareAndSwapInt()方法的声明如下,是一个native方法。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
/**获取原始值*/
var5 = this.getIntVolatile(var1, var2);
/**确认原始值没有被其它线程修改时,再执行更新var5+var4操作*/
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
getAndAddInt()有3个参数,var1为当前传入的对象(count)如果要执行2+1=3这个操作,即var为2(内存值),var4为1,var5(主内存值,如果没有其他线程处理过这个主内存值,那么它应该和va2相等,如果不相等,应该重新从内存中取var2的值)。先判断预存值和内存值是否相等,如果相等,就相加
var1为当前线程,var2为本地内存A的缓存的值,var5为主内存的当前值,如果原始值没有被其他内存修改,才执行var5+var4,如果var2的值和var5的值不相同了,就重新才主内存中或者var2的值。
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
本段代码中的var2存的是对象的内存偏移量,对象调用了objectFieldOffset()方法用于获取某个字段相对Java对象的“起始地址”的偏移量,以获得他的值。