1. 背景
在JDK 5之前Java语言是靠 synchronized 关键字保证同步的,这会导致有锁。锁机制存在以下问题:
-
在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
-
一个线程持有锁会导致其它所有需要此锁的线程挂起。
-
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
Volatile关键字能够在并发条件下,强制将修改后的值刷新到主内存中来保持内存的可见性。通过 CPU内存屏障禁止编译器指令性重排来保证并发操作的有序性
如果多个线程同时操作 Volatile 修饰的变量,也会造成数据的不一致。
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) Thread.yield(); System.out.println(test.inc); } }
事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。
假如某个时刻变量 inc 的值为10:
-
线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;
-
然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
-
然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
-
那么两个线程分别进行了一次自增操作后,inc只增加了1。
之所以出现还是 volatile 只是保证读写具有原子性,但是对于 ++ 操作的复合操作是不存在原子操作的。只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,比如:对变量的写操作不依赖于当前值。
volatile 是不错的机制,但是 volatile 不能保证原子性。因此对于同步最终还是要回到锁机制上来。
独占锁是一种悲观锁,synchronized 就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是 CAS,Compare and Swap。
2. CAS 原理
CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制。CAS 操作包含三个操作数 -- 内存位置、预期数值和新值。CAS 的实现逻辑是将内存位置处的数值与预期数值想比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作。
在 Java 中,Java 并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的。Java 代码需通过 JNI 才能调用。
CAS 是一条 CPU 的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg
对 java.util.concurrent.atomic 包下的原子类 AtomicInteger 中的 compareAndSet 方法进行分析,相关分析如下:
public class AtomicInteger extends Number implements java.io.Serializable { // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { // 计算变量 value 在类对象中的偏移