为了确保数据的线程安全,使用"锁"是最直接的方式。但是,在高并发时,对"锁"的激烈竞争可能会成为系统的瓶颈。
为此,我们提出了一种非阻塞同步的方法,这种方法不需要锁(因此,称之为无锁),但依然能确保数据在高并发环境下保持多线程之间的一致性。
我们接下来讲的就是,这种无锁的同步方法,是怎么实现的?以及怎么用?
非阻塞的同步/无锁
这一段只是概述,如果对CAS有个基本的了解,这一段不用看;
基于锁的同步方式,无论是使用信号量,重入锁还是内部锁,受到临界区的限制,不同线程在竞争锁的时候,是避免不了相互等待,从而阻塞当前线程的。
最简单的非阻塞同步就是ThreadLocal,每个线程都有自己独立的变量副本,因此无需相互等待;
接下来,我们介绍一种更重要的,基于比较并交换(Compare And Swap)CAS算法的无锁并发控制方法。
与锁的方案相比,无锁算法的设计和实现更加复杂,但由于其非阻塞性,它是不会遇到死锁问题的,并且,线程间的相互影响也远远比基于锁的方式要小。
更重要的是,无锁的方式没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它的性能更强;
------------------------------------
CAS算法的过程如下:它包含三个参数CAS(V,E,N),V表示要更新的变量,E表示预期值,N表示新值。只有V==E时,才会把V设置为N。 如果V != E,这说明已经有其他线程做了更新了,那么当前线程什么都不做。
最后,CAS返回当前V的值。
当多个线程同时使用CAS操作一个变量时,只会有一个线程胜出,其余的都会失败。失败的线程不会挂起,仅仅是被告知失败,并且允许再次进行尝试,当前也允许失败的线程放弃操作。
原子操作
java.util.concurrent.atomic包下,有一组使用无锁算法实现的原子操作类,如AtomicInteger,AtomicIntegerArray,AtomicLong,AtomicLongArray,AtomicReference等,分别封装了对整数,整数数组,长整型,长整型数组,普通对象的多线程安全操作。
以AtomicInteger的getAndSet()为例子,讲一下CAS算法是如何工作的;
public final int getAndSet(int newValue) {
for(;;){ //当受到其他线程的影响而更像失败时,会不停地尝试,直到成功
int current = get(); //获得当前值
if(compareAndSet(current,newValue)){ //这里就尝试更新了,如果预期值等于current,就更新
//如果当前值未受其他线程的影响,则设置了新值,并返回旧值
return current;
}
}
}
在整个更新过程中,无需加锁,无需等待。无锁的操作实际上是将多线程并发的冲突处理交由应用层自行解决了。这提高了性能,但增加了算法的复杂度。
-------------------------------
java.util.concurrent.atomic包下的原子类的性能是远远比普通的有锁操作好的,推荐使用它。
可以看看Amino框架