一、序言
前面我们提到的synchronized 等锁机制是一种阻塞同步,虽然它完成了我们的原子性操作,和线程安全,但是这种阻塞同步机制是比较耗费性能的,因为在阻塞和唤醒等状态转换中,是需要CPU指令进行帮忙实现,这要的调度是比较耗时的,因此这种策略是一种悲观策略,当然我们需要线程安全,又要高效,在一定情况下我们会采用非阻塞同步机制。
二、非阻塞同步
它的原理机制是基于冲突检测的乐观锁并发策略,简单的理解就是我们先干了再说,如果没有其他线程访问,那么我们的操作就顺利的完成,如果有其他线程访问,并且产生了冲突,那么我们就再来解决冲突。这样就不用把其他线程阻塞,大量的的进行线程状态的切换,这种操作就是非阻塞同步。
2.1 常见的非阻塞同步有:
a.volatile 变量:轻量级的线程同步,不会引起线程调度,提供可见性,但是不提供原子性
b.CAS 原子指令:轻量级线程同步,不会引起线程调度,提供可见性和原子性。
2.2 CAS 指令:
CAS 是建立在“硬件指令集”上来控制原子性的,因为我们要检测和解决多线程的原子性操作,又不想用阻塞机制,那么只能通过机器指令来完成,这里的指令操作应该是比上面锁机制的指令切换快的(我没做过硬件级的测试)。常用的原子指令操作有:
a.测试并设置(Test-and-Set)
b.获取并增加(Fetch-and-Increment)
c.交换(swap)
d:比较并交换(compare-and-swap,CAS)
e:加载链接/条件存储(Load-Linked/Stroe-Conditional,LL/SC)
后面两种是现在计算机常用的,还其他的指令,这里暂时不介绍了。我们知道提高并发性,应使得串行部分达到最大程度的并行,与锁相比,飞阻塞算法机制,是直接操作机器指令,从指令层面协调多线程的竞争,使得线程在竞争公共资源的时候不会发生阻塞,减少了线程调度的开销,因此速度是由于锁的。
2.3 CAS 的实现
非阻塞线程的实现,以CAS 为例,它需要3个操作数
a.变量的内存地址 A
b.变量的旧的预期值 V
c.变量的新值 B
在CAS 指令执行时,当V 的值符合A的预期的时候,新值B 才会更新,否则不会更新,这是一个原子操作。比如:A 地址指向V = 1,当一个线程准备更新V的值为B=2 额时候,检测到A 还是指向 V= 1的,那么允许修改,如果修改期间,检测到 A 已经指向 V = 其他值的时候,也就是说被其他线程改了就不会执行更新了。
三、非阻塞容器
在JDK 1.5 之后,JAVA 给我们提供了一些非阻塞容器,该操作由sun.misc.Unsafe 类里面的compareAndSwapInt 和 compareAndLong 等几个方法包装,JVM 内部对该方法做了特殊处理,及时编译出来的结果就是平台处理器相关的CAS指令。
常用的非阻塞算法的容器包括:ConcurrentLinkedQueue,SynchronousQueue,Exchanger 和 ConcurrentSkipListMap,包括J.U.C里面的整数原子类AtomicInteger等。在这里我们用一个简单的例子来尝试该原子类的用法,以及从其源码上分析它如何实现的。
public static AtomicInteger race = new AtomicInteger();
public static void main(String[] args) {
// 这是自增方法,我们看看如何实现了 类似 i++ 的功能。
race.incrementAndGet();
}
public final int incrementAndGet() {
for (;;) {
// 先获得当前值
int current = get();
// 然后计算增加后的值,也就是我们的预期值
int next = current + 1;
// 然后预期值和 当前值进行比较(看下面)
if (compareAndSet(current, next))
return next;
}
}
public final int get() {
return value;
}
// 这里是通过当前类在内存中的值,valueOffset,
// 期望的值expect,需要更新的值update
// 进行比较,如果valueOffset 表示的值,和当前except 值等,那么我们执行修改,
// 否则表示已经被其他线程更改了,不执行,循环继续。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
关于valueOffset 的值的取得,是通过Unsafe.getUnsafe()获得实例的 objectFieldOffset 方法获得的,是native 方法,JAVA 仅仅允许启动类加载(Bootstrap ClassLoader) 的类才能访问,反射也可以的,这是它内部代码:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 这里可以定位当前类字段value在内存中的值
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
关于Unsafe 的源码可参考:http://www.docjar.com/html/api/sun/misc/Unsafe.java.html
3.1 CAS 的ABA 问题:
从上面CAS 的原理分析,假设变量i 的原始值是i=5,A 线程通过get() 方法,获取值等于V,然后这时候B线程用同样的方式获得V,然后改成了6,(中途可能被其他使用),然后又改回成5.这时候 A线程去判断的时候,发现内存值还是5,说明没有改变,就执行更新。但是我们发现 在中途其实已经改变过了,又改变回来了而已,这就是ABA 问题。
当然ABA 问题,表面上上不会影响你的业务逻辑,但是在有些情况下,发生这种中途 “调包” 的事情,还是会有影响。解决类似的问题的办法一般是加个版本号,更新了版本加1,每次比较的之后还要对版本进行比较,在JDK 里面是通过院子引用类“AtomicStampedReference” 进行处理的,具体的原理,我也没去看!
小结:
1.非阻塞同步,是通过底层指令,通过比较 交换等操作,保证变量安全。它的算法其实比较复杂的,这里仅仅对原子类进行分析,介绍了CAS 的原理,像上面提到的各种集合类等等,以后再介绍吧。
2.非阻塞算法种类有很多,我在并发实践上看,还有CAS2或者CASX操作的。但是这些 都没做介绍,关于这块的使用,虽说在一定程度上代替了锁机制,但是我认为仅仅能代替简单类型的变化操作,复杂的还是得用锁机制 以及 复杂的算法。