有一类特定的应用,通常是在原有值得基础上做修改,即获取原有值,计算新值,更新。自增就是一个典型的例子。常见的问题比如计数器问题。
在多线程环境下,这个问题当然需要考虑同步,使用syn或者锁肯定可以实现,不过还存在另一种实现方式,CAS。
之前介绍过悲观锁和乐观锁的机制,使用syn锁,是悲观锁,因为要获取锁,然后独占,其余的线程会被阻塞。上述的CAS方式是一种乐观锁机制,本身不需要锁即可同步,而且是非阻塞式的,因为这里没有锁的概念。那么下面就看看它是怎么做到的。
通俗的来讲,乐观锁的实现必然有两个读取,在更新操作刚开始要读取一次,在进行原子更新操作之前又要读取一次,这中间可能会有一些对原有值得计算过程,在更新前,如果检测到两个值不同,说明期间有其他进程更新过,返回执行失败;否则更新成功。这就是大致的流程或者原理。
Java中有针对这种计数器的实现类。主要是concurrent包里面的atomic类。
图片来自别人博客。后面会写。下面以AtomicInteger类来介绍究竟是如何不用syn实现同步更新的。
该类有一个方法getAndIncrement,用于自增。显然这个方法要考虑并发问题。
这里实现的机制叫做CAS,comare and swap。什么意思呢?说白了就是乐观锁。每一次更新前,先拿到当前值expectedValue,然后再计算出更新以后的新值newValue,最后更新,真正的更新操作需要拿到更新变量的地址,看一下这时的值与expectedValue是否一样,一样则更新,否则不更新。这个过程大致是这样的:
这只是一个伪代码。真正的更新操作是在update方法里面完成的。这个方法是同步的,而且是硬件级别的同步。事实上,这个方法也必须同步,否则肯定会有并发问题。所以说,CAS并不是没有同步,只不过是没有使用软件的同步,而使用的是硬件级别的同步。这个update方法会拿到变量的地址,从而得到了当前执行时刻变量的值,与expectedValue对比,从而决定是否更新为newValue。
Java的unsafe类中的方法提供了CAS的函数接口,底层是调用c语言实现的系统调用。因为unsafe中的方法太底层,因此用户无法直接调用。
下面看源码。
这是AtomicInteger的自增实现方法,与前面分析的一样,就是计算期望值和新值,然后做一个系统调用,知道检测到调用成功为止。
这就是系统调用,传入this,我猜测是通过反射等机制拿到变量的物理地址,offset指的是实例变量value在对象中物理偏移量,代码如下:
expect是原值的大小,update是更新后的大小。
unsafe的compareAndSet函数的执行是硬件同步的,整条语句是原子的,只有更新时,值为expect才会更新成功。底层的CAS指令对应了三个操作数,变量的地址,期望值和新值。最终返回是否执行成功。
效率方面,在中低并发情况下,原子类效果好,但是在高并发下,锁机制更好。