concurrent包的基础:CAS

前言

java.util.concurrent包的实现,大大提高了并发编程的效率。
相对于传统的synchronized类似悲观锁的实现(jdk1.6后优化,效率也大幅提升,后续总结),并发包concurrent依赖CAS提供了乐观锁的解决方案,使得效率的大幅提升。

悲观锁和乐观锁

多个线程,共同访问一个共享变量:
悲观锁:每个线程都去抢这个共享变量,并且认为自己用的时候,别人如果看到了,也会来改变这个变量,影响自己(被迫害幻想症,当然也的确有人不是只来看看,会改变该变量,造成错误),所以抢到的人把变量带回家,自己一个人使用,用完才拿出来,让后续线程使用;
乐观锁:把这个共享变量放在广场中央,大家共享,谁都可以过来,并且认为,有的线程来,也就是看看并不改变这个变量,即使其他线程改变了,也无所谓,大家都使用CAS操作,也不会发生错误。

乐观锁效率一般来说高,因为很多线程的确只是来读值,并且省略了把变量带回家,关门,开门,送回广场的步骤;这里要给synchronized正名,jdk1.6优化,考虑到这些情况,给予了优化。

那么,这里关键就是这个CSA技术。如何做到大家都来改值,还不会出现错误的?

CAS

CAS:Compare and Swap, 比较并交换。
CAS有3个操作数,内存值V,预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

CAS并不是java中的专有名词,他是属于计算机体系的,一般的计算机系统都在硬件层次上直接支持CAS指令,java借助这些特殊指令,实现乐观锁。

下面借助原子类AtomicInteger的实现原理,来理解CAS。

我们知道:

  1. 一个变量 int a = 0;
  2. 100个线程同时来更新a,操作:a++
  3. 结果,a的值小于100

原因:
a++操作不是原子的,其实是分了3步。主内存读取a的值至工作线程的缓存;cpu对a做加1的计算;a写回工作线程的缓存,再择时写会主内存。(涉及的知识:jvm内存模型,推荐书籍《Java编程的艺术》)
多线程访问,可能有的计算没有及时写会主内存,造成覆盖,最终结果小于100。

我们使用原子类AtomicInteger的incrementAndGet()方法,以原子方式获取旧值并给当前值加1,也就是上面例子结果会是100。

原子类AtomicInteger部分源码:【理解这套思想,有助于理解concurrent包中其它类】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Unsafe,包名sun.misc.Unsafe,调用操作系统的cas指令
private static final Unsafe unsafe = Unsafe.getUnsafe();
//volatile修饰符的2个作用,1.保存内存可见性;2.禁止指令重排。明显使用的作用1
private volatile int value;
//内部实现
public final int incrementAndGet() {
for (;;) {//死循环,
//获取保证了内存可见行的value
int current = get();
int next = current + 1;
//使用cas操作更新,更新成功后,返回更新后的值
//get()方法获取current后,其它线程没有更新,内存中的值和期望的值current一致,
//则修改为加1后的next;反之,则什么都不做,进入下轮循环,直至更新成功。
if (compareAndSet(current, next))
return next;
}
}
//调用了Unsafe的方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

CAS存在ABA的问题

一个线程开始看到的值是A,随后使用CAS进行更新,它的实际期望是没有其他线程修改过才更新,但普通的CAS做不到,因为可能在这个过程中,已经有其他线程修改过了,比如先改为了B,然后又改回为了A。

解决方案:可以使用JDK的并发包中的AtomicStampedReference和 AtomicMarkableReference来解决。

文章目录
  1. 1. 前言
  2. 2. 悲观锁和乐观锁
  3. 3. CAS
  4. 4. CAS存在ABA的问题
|