多线程-锁的类型 && CAS 非阻塞同步

来源:互联网 发布:网络律师 编辑:程序博客网 时间:2024/05/01 16:11

锁的类型

根据完成任务所需要的结构不同,一个应用程序可能会使用多种不同类型的锁原语,因此,开发人员必须避免在一个给定任务中混淆这些锁原语,尤其在使用第三方提供的库时需要格外小心。假设在一个应用程序中,资源R依赖于第三方提供的库,并且在该库中资源R使用L型锁作为同步原语。此时,若开发者需要在应用程序中对资源R使用锁原语,则必须也使用L而不是其他类型的锁原语。接下来的部分将介绍不同类型的锁,并详细说明它们的用途。

互斥量(Mutex)  互斥量是实现最简单的锁类型,因此有一些教科书一般以互斥量为例对锁原语进行描述。互斥量的释放并不仅仅依赖于释放操作,还可以引入一个定时器属性。如果在释放操作执行前发生定时器超时,则互斥量也会释放代码块或共享存储区供其他线程访问。当有异常发生时,可使用try-finally语句来确保互斥量被释放。定时器状态或try-finally语句的使用可以避免产生死锁。

递归锁(Recursive Lock)  递归锁是指可以被当前持有该锁的线程重复获取,而不会导致该线程产生死锁的锁类型。对递归锁而言,只有在当前持有线程的获取锁操作都有一个释放操作与之对应时,其他线程才可以获取该锁。因此,在使用递归锁时,必须要用足够的释放锁操作来平衡获取锁操作,实现这一目标的最佳方式是在单入口单出口代码块的两头一一对应地使用获取、释放操作,做法和在普通锁中一样。递归锁在递归函数中最有用。但是,总的来说,递归锁比非递归锁速度要慢。需要注意的是:调用线程获得几次递归锁必须释放几次递归锁。
图4-10给出了一个递归锁的使用范例。

Recursive_Lock L

void recursiveFunction (int count) {

L->acquire()

if (count > 0) {

count = count - 1;

recursiveFunction(count);

}

L->release();

}

图4-10  递归锁使用范例

读写锁(Read-Write lock) 读写锁又称为共享独占锁(shared-exclusive lock)、多读单写锁(multiple-read/single-write lock)或者非互斥信号量(non-mutual exclusion semaphore)。读写锁允许多个线程同时进行读访问,但是在某一时刻却最多只能由一个线程执行写操作。对于多个线程需要同时读共享数据却并不一定进行写操作的应用来说,读写锁是一种高效的同步机制。对于较长的共享数据,只为其设置一个读写锁会导致较长的访问时间,最好将其划分为多个小段并设置多个读写锁以进行同步。

旋转锁(Spin Lock) 旋转锁是一种非阻塞锁,由某个线程独占。采用旋转锁时,等待线程并不静态地阻塞在同步点,而是必须“旋转”,不断尝试直到最终获得该锁。旋转锁多用于多处理器系统中。这是因为,如果在单核处理器中采用旋转锁,当一个线程正在“旋转”时,将没有执行资源可供另一释放锁的线程使用。旋转锁适合于任何锁持有时间少于将一个线程阻塞和唤醒所需时间的场合。线程控制的变更,包括线程上下文的切换和线程数据结构的更新,可能比旋转锁需要更多的指令周期。旋转锁的持有时间应该限制在线程上下文切换时间的50%到100%之间(Kleiman,1996年)。在线程调用其他子系统时,线程不应持有旋转锁。对旋转锁的不当使用可能会导致线程饿死,因此需谨慎使用这种锁机制。旋转锁导致的饿死问题可使用排队技术来解决,即每个等待线程按照先进先出的顺序或者队列结构在一个独立的局部标识上进行旋转



一、序言

       前面我们提到的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等。在这里我们用一个简单的例子来尝试该原子类的用法,以及从其源码上分析它如何实现的。

         

Java代码  收藏代码
  1. public static AtomicInteger race = new AtomicInteger();  
  2.   
  3. public static void main(String[] args) {  
  4.     // 这是自增方法,我们看看如何实现了 类似 i++ 的功能。  
  5.     race.incrementAndGet();  
  6. }  

 

Java代码  收藏代码
  1.   public final int incrementAndGet() {  
  2.         for (;;) {  
  3.             // 先获得当前值  
  4.             int current = get();  
  5.             // 然后计算增加后的值,也就是我们的预期值  
  6.             int next = current + 1;  
  7.             // 然后预期值和 当前值进行比较(看下面)  
  8.             if (compareAndSet(current, next))  
  9.                 return next;  
  10.         }  
  11.     }  
  12.   
  13.   public final int get() {  
  14.         return value;  
  15.     }  
  16.   
  17. // 这里是通过当前类在内存中的值,valueOffset,  
  18. // 期望的值expect,需要更新的值update  
  19. // 进行比较,如果valueOffset 表示的值,和当前except 值等,那么我们执行修改,  
  20. // 否则表示已经被其他线程更改了,不执行,循环继续。  
  21. public final boolean compareAndSet(int expect, int update) {  
  22.     return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
  23.     }  

 

    关于valueOffset 的值的取得,是通过Unsafe.getUnsafe()获得实例的 objectFieldOffset 方法获得的,是native 方法,JAVA 仅仅允许启动类加载(Bootstrap ClassLoader) 的类才能访问,反射也可以的,这是它内部代码:

      

Java代码  收藏代码
  1. private static final Unsafe unsafe = Unsafe.getUnsafe();  
  2.    private static final long valueOffset;  
  3.   
  4.    static {  
  5.      try {  
  6.        // 这里可以定位当前类字段value在内存中的值  
  7.        valueOffset = unsafe.objectFieldOffset  
  8.            (AtomicInteger.class.getDeclaredField("value"));  
  9.      } catch (Exception ex) { throw new Error(ex); }  
  10.    }  

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


0 0