以AtomicInteger为例,jdk版本1.8
先举个例子
public class AtomicIntegerTest {
private static int threadCount = 10;
private static CountDownLatch countDown = new CountDownLatch(threadCount);
private static int count = 0;
public static void main(String[] args) {
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(new Counter());
}
for (int i = 0; i < threadCount; i++) {
threads[i].start();
}
try {
countDown.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
private static class Counter implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count++;
}
countDown.countDown();
}
}
}
在这个例子中,我们开启了10个线程,来增加count的值,期待最后输出的结果是10000。显然,并不是每次运行的结果都是10000。因为多个线程对count的修改操作并不是原子操作。
某次运行的输出结果
count=9262
我们可以使用AtomicInteger
来解决这个问题。
public class AtomicIntegerTest {
private static int threadCount = 10;
private static CountDownLatch countDown = new CountDownLatch(threadCount);
private static AtomicInteger count = new AtomicInteger(0);//原子操作类
public static void main(String[] args) {
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(new Counter());
}
for (int i = 0; i < threadCount; i++) {
threads[i].start();
}
try {
countDown.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
private static class Counter implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
//原子操作
count.getAndIncrement();
}
countDown.countDown();
}
}
}
现在每次打印的结果都是10000。因为AtomicInteger类的getAndIncrement方法是原子操作。
运行的输出结果
count=10000
AtomicInteger类中的一些变量
//unsafe实例,用来获取并操作内存的数据。
private static final Unsafe unsafe = Unsafe.getUnsafe();
//用来记录偏移量,这是一个final变量
private static final long valueOffset;
static {
try {
//valueOffset默认值是0
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//value,存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。
private volatile int value;
构造函数
/**
* 使用0作为初始值来构建AtomicInteger实例
*/
public AtomicInteger() {
}
/**
* 使用指定的初始值构建AtomicInteger实例
*
* @param initialValue 初始值
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
AtomicInteger的getAndIncrement方法
/**
* 原子性的把当前值加1。
*
* @return 返回以前的值
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
Unsafe的getAndAddInt方法
/**
* 原子性的更新一个对象在偏移量为offset处的成员变量的值,或者原子性的更新一个数组在偏移量为offset处的元素的值。
*
* @param o 更新成员变量的对象,或者更新元素的数组
* @param offset 成员变量或者数组元素的偏移
* @param delta 要增加到的量
* @return 先前的值
* @since 1.8
*/
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
//do while 自旋操作
do {
//获取AtomicInteger对象在内存中偏移量为offset处的值
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
Unsafe的getIntVolatile方法
/** Volatile version of {@link #getInt(Object, long)} */
public native int getIntVolatile(Object o, long offset);
Unsafe的compareAndSwapInt方法,这个方法在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。
/**
* 如果Java变量的当前值是expected,则原子性的把该变量的值更新为x。
* @return 成功返回true
*/
public final native boolean compareAndSwapInt(Object o, long offset,int expected, int x);
用文字叙述一下getAndAddInt这个过程。我们假设现在只有两个线程,一个是主线程,一个A线程
-
在主线程第1次进入 do while循环,执行
v = getIntVolatile(o, offset);
获取到的AtomicInteger的value是0赋值给v。 -
执行
compareAndSwapInt(o, offset, v, v + delta);
如果此时A
线程没有修改AtomicInteger的value,那么获取AtomicInteger对象在内存中偏移量为offset处的value和v相等,执行成功,将AtomicInteger的value更新为1,返回true,循环结束。然后返回AtomicInteger的更新之前的value,就是0。 -
执行
compareAndSwapInt(o, offset, v, v + delta);
如果此时A线程修改了AtomicInteger的value(value值为1),
那么获取AtomicInteger对象在内存中偏移量为offset处的值和v已经不相等了(内存中的值已经不变成1了,而v是0),执行失败,进入下一次循环。 -
在主线程第2次进入 do while循环,执行
v = getIntVolatile(o, offset);
,因为A线程修改了AtomicInteger的value,获取到的AtomicInteger的value是1赋值给v。 -
执行
compareAndSwapInt(o, offset, v, v + delta);
如果此时A
线程没有修改AtomicInteger的value,那么获取AtomicInteger对象在内存中偏移量为offset处的值和v相等(都是1),执行成功,将AtomicInteger的value更新为2,返回true,循环结束。然后返回AtomicInteger的更新之前的value,就是1。
上面的循环可以保证,在不同的线程更新value值不会出现被覆盖的情况。也就是说实现了原子性操作。
CAS 的问题
- ABA问题
CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是CAS的ABA问题。
常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
-
循环时间长开销大
上面我们说过如果CAS不成功,则会原地自旋(一直 do while),如果长时间自旋会给CPU带来非常大的执行开销。 -
只能保证一个共享变量的原子操作。
对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
参考链接:
网友评论