举个栗子:
原子性问题我们前面一直都是采用的互斥锁方案。 其实对于简单的原子性问题,还有一种无锁方案。Java SDK 并发包将这种无锁方案封装提炼之后,实现了一系列的原子类。
下面 Test中add的方法在,在多线程的情况下count最终可能<100000;因为 add 方法是非线程安全的。
1 public class myTest { 2 long count = 0; 3 void add(){ 4 int idx = 0; 5 while(idx++ < 100000) { 6 count += 1; 7 } 8 }
}
在下面的代码中,我们将原来的 long 型变量 count 替换为了原子类 AtomicLong,原来的 count+=1替换成了 count.getAndIncrement(),仅需要这两处简单的改动就能使add() 方法变成线程安全的,原子类的使用还是挺简单的。
1 public class myTest { 2 AtomicLong count = new AtomicLong(0); 3 void add(){ 4 int idx = 0; 5 while(idx++ < 100000) { 6 count.getAndIncrement(); 7 } 8 } 9 }
As you know(AYouK), 互斥锁方案为了保证互斥性,需要频繁的执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;若拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题,可谓绝佳方案。可见它最大的好处就是性能。那它是如何做到的呢?
不得不说的CAS(Compare And Swap,即“比较并交换”,自旋锁等等)
很多编程语言或者系统实现上都大量的使用了CAS。其实原子类性能高的秘密很简单,硬件支持而已。CPU 为了解决并发问题,提供了 CAS 指令。CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;并且只有当内存中地址 A 处的值等于 B时,才能将内存中地址 A 处的值更新为新值 C。
下面截取了GetAndIncrement的部分源码,调用了unsafe的 getAndAddLong方法。
Unsafe提供了三个方法用于CAS操作,分别是
1 public final native boolean compareAndSwapObject(Object value, long valueOffset, Object expect, Object update); 2 3 public final native boolean compareAndSwapInt(Object value, long valueOffset, int expect, int update); 4 5 public final native boolean compareAndSwapLong(Object value, long valueOffset, long expect, long update);
- value 表示 需要操作的对象
- valueOffset 表示 对象(value)的地址的偏移量(通过
Unsafe.objectFieldOffset(Field valueField)
获取) - expect 表示更新时value的期待值
- update 表示将要更新的值
总结
无锁方案相对于互斥锁方案,优点非常多,首先性能好,其次是基本不会出现死锁问题(但可能出现饥饿和活锁问题,因为自旋会反复重试)。Java 提供的原子类大部分都实现了compareAndSet() 方法,基于 compareAndSet() 方法,你可以构建自己的无锁数据结构,但是建议你不要这样做,因为无锁算法没你想象的那么简单。Java 提供的原子类能够解决一些简单的原子性问题,但你可能会发现,上面我们所有原子类的方法都是针对一个共享变量的,如果你需要解决多个变量的原子性问题,建议还是使用互斥锁方案。
原子类虽好,但使用要慎之又慎。
public static void main(String[] args) {
long count = 0;
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
System.out.println(count);
}