`
greemranqq
  • 浏览: 967040 次
  • 性别: Icon_minigender_1
  • 来自: 重庆
社区版块
存档分类
最新评论

多线程(四)--非阻塞同步,CAS 原理分析

阅读更多

一、序言

       前面我们提到的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操作的。但是这些 都没做介绍,关于这块的使用,虽说在一定程度上代替了锁机制,但是我认为仅仅能代替简单类型的变化操作,复杂的还是得用锁机制 以及 复杂的算法。

         

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics