在Java多线程操作中,为了数据的同步,我们会对需要同步的操作进行加锁处理。比如如下的一个计数器。
class Counter
{
private int i=0;
public synchronized int get()
{
return this.i;
}
public synchronized void increase()
{
this.i++;
}
}
为了避免多线程环境下,对数据操作发生混乱的情况,我们使用了synchronized
关键字.
synchronized
Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这个段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块
当有某线程占用了临界资源(这里是Counter对应的实例对象),其余的线程都会被阻塞,直到占用该临界资源的锁释放。
这里引发了一个线程开销问题:阻塞的线程会被中断,当阻塞的线程重新得到运行时,需要进行线程上下文的切换。线程上下文切换在现在操作系统下是一个比较耗时的操作。
解决方法: 使用CAS
实现
CAS (CompareAndSwap)
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。CAS操作在现代操作系统中是一条原子语句:可以由一条计算机指令完成。
假设CAS对应的操作为:CAS(V,A,B) 当且仅当V内存对应的值为A时候,将V内存中的值修改为B。
非阻塞算法 (nonblocking algorithms)
一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。
拿出AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的。
private volatile int value;
首先毫无以为,在没有锁的机制下可能需要借助volatile
原语,保证线程间的数据是可见的(共享的)。
这样才获取变量的值的时候才能直接读取。
public final int get() {
return value;
}
然后来看看++i是怎么做到的。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在这里采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
而compareAndSet
利用JNI来完成CPU指令的操作。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
整体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。
借助CAS
,Counter可以修改为:
class Counter
{
private AtomicInteger i=new AtomicInteger(0);
public Integer get()
{
return i.get();
}
public void increase()
{
for(;;)
{
int value=i.get();
if(i.compareAndSet(value,value+1))
{
break;
}
}
}
}