CAS无锁实现原理
为什么要用CAS
在多线程高并发编程的时候,最关键的问题就是保证临界区的对象的安全访问。通常是用加锁来处理,其实加锁本质上是将并发转变为串行来实现的,势必会影响吞吐量。而且线程的数量是有限的,依赖于操作系统,而且线程的创建和销毁带来的性能损耗是不可以忽略掉的。虽然现在基本都是用线程池来尽可能的降低不断创建线程带来的性能损耗。
对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。而无锁是一种乐观策略,它会假设对资源的访问时没有冲突的,既然没有冲突就不需要等待,线程不需要阻塞。那多个线程共同访问临界区的资源怎么办呢,无锁的策略采用一种比较交换技术CAS(compare and swap)来鉴别线程冲突,一旦检测到冲突,就充实当前操作指导没有冲突为止。
与锁相比,CAS会使得程序设计比较负责,但是由于其优越的性能优势,以及天生免疫死锁(根本就没有锁,当然就不会有线程一直阻塞了),更为重要的是,使用无锁的方式没有所竞争带来的开销,也没有线程间频繁调度带来的开销,他比基于锁的方式有更优越的性能,所以在目前被广泛应用,我们在程序设计时也可以适当的使用.
不过由于CAS编码确实稍微复杂,而且jdk作者本身也不希望你直接使用unsafe
(后面会讲到)来进行代码的编写,所以如果不能深刻理解CAS以及unsafe
还是要慎用,使用一些别人已经实现好的无锁类或者框架就好了。
CAS原理分析
CAS算法
一个CAS方法包含三个参数CAS(V,E,N)
。V表示要更新的变量,E表示预期的值,N表示新值。只有当V的值等于E时,才会将V的值修改为N。如果V的值不等于E,说明已经被其他线程修改了,当前线程可以放弃此操作,也可以再次尝试次操作直至修改成功。基于这样的算法,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰(临界区值的修改),并进行恰当的处理。
- 额外引申技术点:volatile
上面说到当前线程可以发现其他线程对临界区数据的修改,这点可以使用volatile
进行保证。 volatile
实现了JMM中的可见性。使得对临界区资源的修改可以马上被其他线程看到,它是通过添加内存屏障实现的。具体实现原理请自行搜索volatile
AtomicInteger
初次接触CAS的人一般都是通过AtomicInteger
这个类来了解的,这里讲其原理也借助这个类。
查看一下AtomicInteger
的源码:
private volatile int value;
//此处省略一万字代码
/**
* Atomically sets to the given value and returns the old value.
*
* @param newValue the new value
* @return the previous value
*/
public final int getAndSet(int newValue) {
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return true if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
通过这段代码可知:
- AtomicInteger
中真正存储数据的是value
变量,而改变量是被volatile
修饰的,保证了线程直接的可见性。还记得Integer
中的value吗?Integer
中的value
是被final
修饰的,是不可变对象。
- getAndSet
方法通过一个死循环不断尝试赋值操作。而真正的赋值操作交给了unsafe
类来实现。
unsafe
上面可知,Unsafe
类是CAS实现的核心。
从名字可知,这个类标记为不安全的,JDK作者不希望用户使用这个类,我们看一下他的构造方法:
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if(var0.getClassLoader() != null) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
如果ClassLoader
不是null,直接抛出异常了,我们没办法在应用程序中使用这个类
public static void main(String[] args){
Unsafe unsafe = Unsafe.getUnsafe();
}
main方法运行结果:
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.le.luffi.Tewast.main(Tewast.java:13)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
我们来看一下compareAndSwapInt
的方法声明
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
第一个参数是给定的对象,offset是对象内的偏移量(其实就是一个字段到对象头部的偏移量,通过这个偏移量可以快速定位字段),第三个参数是期望值,最后一个是要设置的值。
其实这里Unsafe
封装了一些类似于C++中指针的东西,该类中的方法都是native
的,而且是原子的操作。原子性是通过CAS原子指令实现的,由处理器保证。