- 乐观锁
- 总是假设最好的情况,认为并发安全问题一定不会发生,所以不需要加锁。
- 每次去拿数据的时候都认为别人不会修改,所以不会上锁。
- 但是在最终更新数据的时候,会判断一下在此期间别人有没有更新过这个数据,如CAS算法的实现。
- CAS算法主要分成3步:
-
- 获取共享数据的值,拷贝副本到工作内存,保存到值 current
- 对工作内存中的共享数据进行操作,获得更新后的值 next
- 执行CAS (compare and set)操作,先去主存获取当前共享变量的值value,与自己的旧值current比较
- 如果二者相同,表示这段时间没有其他线程修改共享变量值,则同步 值 next 到主存
- 如果二者不同,表示这段时间有其他线程修改了共享变量值,则放弃本轮修改,重新执行整个CAS。
- 因此CAS一般写成一个while循环,修改成功则退出循环,否则就重试
- 如 AtomicBoolean、AtomicInteger、AtomicLong 就是这个干的
CAS 比较与交换算法
在CAS(比较与交换)算法中涉及3个操作数:变量当前内存值V、变量的预期值E、新值U。只有该变量当前的内存值V与预期值E相同时,才会将新值U写入内存完成变量修改,否则什么都不做。下面是通过CAS修改变量数据的示例,CAS通过该变量的地址即可获取该变量当前的内存值V。当本轮CAS操作失败后,会重新读取该变量内存中最新的值并重新计算新值,直到其CAS操作修改变量成功为止。
do{
1. 读取变量值,记为E。用于写入修改时,判断该变量是否被修改
2. 使用读到的变量值E,计算该变量的新值,记为U
} while( !CAS(变量内存地址,E,U) )
在Java中,java.util.concurrent.atomic包的原子变量类大量使用了Unsafe类提供的CAS操作。进一步地,CAS操作通过硬件来保证了比较-更新操作的原子性。下面分别使用volatile和AtomicInteger来进行演示
public class CASDemo {
private static AtomicInteger atomicCount = new AtomicInteger(0);
private static volatile Integer count = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[20];
for(int i=0; i<20; i++) {
Thread thread = new Thread( ()->{
for(int j=0; j<10000; j++) {
atomicCount.incrementAndGet(); // atomicCount++
count++;
}
} );
threads[i] = thread;
thread.start();
}
for(Thread thread : threads) {
thread.join();
}
System.out.println("atomicCount: " + atomicCount);
System.out.println("count: " + count);
}
}
从测试结果中,我们可以使用了CAS的原子类具备原子性
实现原理
前面我们说到CAS操作的原子性是通过硬件来保证,这里作进一步的解释说明。CPU层面上,CAS的比较-写入操作上是通过cmpxchg指令去实现完成的。然而不幸的是,cmpxchg指令并不是一个原子操作。即可能会发生这样的场景,线程A在执行cmpxchg指令的过程中,发现当前内存值V与预期值E一致,正准备将新值U写入内存,这个时候另外一个线程B打断了线程A的操作,将该变量修改了。显然这个变量发生了线程安全的问题,为此为了保证cmpxchg指令的原子性,不会被打断,需要在cmpxchg指令前添加一个前缀指令lock。通过对cmpxchg指令进行加锁(总线锁或缓存锁)来保证操作的原子性。通常我们会说CAS算法是一个无锁算法,但其实我们可以看到底层依然是加了锁的,只不过这个锁的粒度是很小的。
- CAS存在ABA问题:
- 当前线程只能感知共享变量的值有没有改变,假设别人从A改成了B,又改成了A,则当前线程感知不到,即ABA问题
- 可以使用版本号机制解决ABA问题,如原子类AtomicStampedReference,加入了版本号,每当修改值,版本号就会+1
- 如 AtomicStampedReference 加入了版本号,每当修改值,版本号就会+1。这样根据版本号就知道有没有别的线程修改过,而且还能知道修改过几次
- 再如 AtomicMarkableReference,他的版本号是一个boolean类型,所以只能判断是否修改过值,不知道修改过几次。
AtomicMarkableReference只能缓解ABA问题,如果别人修改标记后,又改回来,则无法感知。
- 乐观锁适用于多读的应用类型,这样可以提高吞吐量