基础介绍
要对AtomicInteger有一个深入的认识,就必须要了解一下悲观锁和乐观锁。
cpu是时分复用的,也就是把cpu的时间片,分配给不同的线程进程轮流执行,
时间片与时间片之间,需要进行cpu切换,也就是会发生进程的切换。切换涉及到清空
寄存器,缓存数据。然后重新加载新的thread所需数据。
当一个线程被挂起时,加入到阻塞队列,在一定的时间或条件下,在通过
notify(),notifyAll()唤醒回来。在某个资源不可用的时候,就将cpu让出,
把当前等待线程切换为阻塞状态。等到资源(比如一个共享数据)可用了,那么就将线程唤醒,
让他进入runnable状态等待cpu调度。这就是典型的悲观锁的实现。
但是,由于在进程挂起和恢复执行过程中存在着很大的开销。当一个线程正在等待锁时,
它不能做任何事,所以悲观锁有很大的缺点。举个例子,如果一个线程需要某个资源,但是
这个资源的占用时间很短,当线程第一次抢占这个资源时,可能这个资源被占用,如果此时挂起
这个线程,可能立刻就发现资源可用,然后又需要花费很长的时间重新抢占锁,时间代价
就会非常的高。
所以就有了乐观锁的概念,他的核心思路就是,每次不加锁而是假设没有冲突而去完成某项操作,
如果因为冲突失败就重试,直到成功为止。在上面的例子中,某个线程可以不让出cpu,而是一直
while循环,如果失败就重试,直到成功为止。所以,当数据争用不严重时,乐观锁效果更好。
比如我们要说的AtomicInteger底层同步CAS就是一种乐观锁思想的应用。
CAS就是Compare and Swap的意思,比较并操作。很多的cpu直接支持CAS指令。CAS是项
乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,
而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS有3个操作数,内存值V,预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,
将内存值V修改为B,否则什么都不做。
</br>
CAS操作
CAS通过调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。而compareAndSwapInt就是借助C来调用CPU底层指令实现的。#lock类似的cpu指令.
AtomicInteger内部有一个变量UnSafe:
private static final Unsafe unsafe = Unsafe.getUnsafe();
Unsafe类是一个可以执行不安全、容易犯错的操作的一个特殊类。虽然Unsafe类中所有方法都是public的,但是这个类只能在一些被信任的代码中使用。其实CAS指的是sun.misc.Unsafe这个类中的一些方法的统称。例如,Unsafe这个类中有compareAndSwapInt、compareAndSwapLong等方法。
public final native boolean compareAndSwapInt(Object o, long V, int E, int N);
CAS的过程是:它包含了3个参数CAS(O,V,E,N)。O表示要更新的对象。V表示指明更新的对象中的哪个变量,E是进行比较的值,如果V==E,则将N赋值给V。
第二个参数V(offset),其实要更新的对象里的字段相对于对象初始位置的内存偏移量。通俗一点就是在CAS(O,V,E,N)中,O是你要更新那个对象,
V就是我要通过这个偏移量找到这个对象中的value对象,来对他进行操作。也就是说,如果我把1这个数字属性更新到2的话,需要这样调用:
compareAndSwapInt(this, valueOffset, 1, 2);
valueOffset字段表示内存位置,可以在AtomicInteger对象中使用unsafe得到:
static {
try {
valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex); }
}
AtomicInteger内部使用变量value表示当前的整型值,这个整型变量还是volatile的,表示内存可见性,一个线程修改value之后保证对其他线程的可见性:
private volatile int value;
AtomicInteger内部还封装了一下CAS,定义了一个compareAndSet方法,只需要2个参数:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
其中
unsafe.compareAndSwapInt(this, valueOffset, expect, update);
类似于:
if (this == expect) {
this = update;
return true;
} else {
return false;
}
具体函数说明
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe unsafe = Unsafe.getUnsafe(); //这里是初始化一个Unsafe对象。因为CAS是这个类中的方法。
private static final long valueOffset;
static {
try {
/*一个java对象可以看成是一段内存,各个字段都得按照一定的顺序放在这段内存里,
同时考虑到对齐要求,可能这些字段不是连续放置的,用这个方法能准确地告诉你某个
字段(也就是下面的value字段)相对于对象的起始内存地址的字节偏移量,因为是相对
偏移量,所以它其实跟某个具体对象又没什么太大关系,跟class的定义和虚拟机的内
存模型的实现细节更相关。通俗一点就是在CAS(O,V,E,N)中,O是你要更新那个对象,
V就是我要通过这个偏移量找到这个对象中的value对象,来对他进行操作。*/
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//volatile,保证变量的可见性。
private volatile int value;
//有参构造
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
public final int get() {
return value;
}
public final int getAndIncrement() {
//为什么会无限循环,先得到当前的值value,然后再把当前的值加1
//加完之后使用cas原子操作让当前值加1处理正确。当然cas原子操作不一定是成功的,
//所以做了一个死循环,当cas操作成功的时候返回数据。这里由于使用了cas原子操作,
//所以不会出现多线程处理错误的问题。
//比如,
// 1. Thread-A进入循环获取current=1,然后切下cpu,Thread-B上cpu得到current=1再下cpu;
// 2. 然后Thread-A的next值为2,进行cas操作并且成功的时候,将value修改成了2;这时候内存中value值为2了,
// 这个时候Thread-B切上cpu执行的next值为2,当进行cas操作的时候由于expected值已经是2,而不是1了;所以cas操作会失败,
// 3. 失败了怎么办,在下个循环中得到的current就变成了2;也就不会出现多线程处理问题了!
for (;;) {
int current = get(); //得到当前的值
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
CAS缺点
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。
---ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作。
1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
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操作。