在多线程的情况下,对一个值进行 a++ 操作,会出现什么问题?
先写个 demo 的例子。把 a++ 放入多线程中运行一下。定义 10 个线程,每个线程里面都调用 5 次 a++,把 a 用 volatile 修饰,可以让 a 的值在修改之后,所有的线程立刻就可以知道。最后结果是不是 50,还是其他的数字?
public class Test {
private static volatile int a = 0;
public static void main(String[] args) {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(new Runnable(){
@Override
public void run() {
try {
for(int j = 0; j < 10; j++) {
System.out.print(a++ + ", ");
Thread.sleep(100);
}
} catch (Exception e) {
}
}
});
threads[i].start();
}
}
}
从结果上看 a++ 的操作并没有达到预期值的 50,而是少了很多,其中还有一定是有问题的。那就是因为 a++ 的操作并不是原子性的。
并发编程,有三大原则:有序性、可见性、原子性
上面的 a++ 就没有原子性,它有三个步骤:
这三个步骤可以被示例中的 10 个线程上下文切换打断:当 a = 10
从上面的步骤中可以看出 a 的值在两次相加后没有得到 12 的值,而是 11。这就是 a++ 引发的问题。
小 B 把上面的步骤对面试官讲了一遍,面试官又问了,有什么方式可以避免这个问题,小 B 不加思索的回答用 synchronized 加锁。面试官说 synchronized 太重了,还有其他的解决方式吗?小 B 晕了。其实可以使用 AtomicInteger 的 incrementAndGet() 方法。
首先看看 AtomicInteger 的主要属性。
//sun.misc 下的类,提供了一些底层的方法,用于和操作系统交互
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value 字段的内存地址相对于对象内存地址的偏移量
private static final long valueOffset;
//通过 unsafe 初始化 valueOffset,获取偏移量
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 用 valatile 修饰的值,保证了内存的可见性
private volatile int value;
从属性中可以看出 AtomicInteger 调用的是 Unsafe 类,Unsafe 类中大多数的方法是用 native 修饰的,可以直接进行一些系统级别的操作。
用 volatile 修饰 value 值,保证了一个线程的值对另外一个线程立即可见。
//AtomicInteger.incrementAndGet()
public final int incrementAndGet() {
//调用 unsafe.getAndAddInt()
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//Unsafe.getAndAddInt()
//参数:需要操作的对象,偏移量,要增加的值
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
//Unsafe.compareAndSwapInt()
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
incrementAndGet() 首先获取了当前值,然后调用 compareAndSwapInt() 方法更新数据。
compareAndSwapInt() 是 CAS 的缩写来源,比较并替换。被 native 修饰,调用了操作系统底层的方法,保证了硬件级别的原子性。
var2,var4,var5 是它的三个操作数,表示内存地址偏移量 valueOffset,预期原值 expect,新的值 update。把 this.compareAndSwapInt(var1, var2, var5, var5 + var4)
变成 this.compareAndSwapInt(obj, valueOffset, expect, update)
,释义就是如果内存位置中的 valueOffset 值 与 expect 的值相同,就把内存中的 valueOffset 改成 update,否则不操作。
getAndAddInt() 方法中用了 do-while,就相当于如果 CAS 一直更新不成功,就不退出循环。直到更新成功为止。
CAS 操作也并不是没有问题的。
这篇文章介绍了 CAS,它是 java 中的乐观锁,每次认为操作并不会有其他线程去修改数据,如果有其他线程操作了数据,就重试,一直到成功为止。
全部0条评论
快来发表一下你的评论吧 !