疯狂java


您现在的位置: 疯狂软件 >> 新闻资讯 >> 正文

java高并发程序设计模式-并发级别:阻塞、无障碍、无锁、无等待【转载】


 

 

 

一般认为并发可以分为阻塞与非阻塞,对于非阻塞可以进一步细分为无障碍、无锁、无等待,下面就对这几个并发级别,作一些简单的介绍。

 

1、阻塞

阻塞是指一个线程进入临界区后,其它线程就必须在临界区外等待,待进去的线程执行完任务离开临界区后,其它线程才能再进去。

2、无障碍(obstruction-free)

  • 无障碍是一种最弱的非阻塞调度
  • 自由出入临界区
  • 无竞争时,有限步内完成操作
  • 有竞争时,回滚数据

无障碍并不要求一个线程进入临界区之后,其它线程在临界区外等待。所有线程都可以自由地进入临界区,它是一种最弱的非阻塞调度。

跟阻塞调度比较,阻塞调度可以认为是一种悲观的策略,它会认为多个线程一起修改数据会使数据损坏,所以阻塞调度每次只能允许一个线程去修改数据。而非阻塞调度相对来说比较乐观,它认为如果多个线程一起修改也未必会把造成数据损坏,所以它允许多个线程同时进入临界区,但无障碍是一种宽进严出的策略,进的时候不作限制,所有的线程都能进入临界区做其想做的事情,包括读与写,但是出来的时候就不那么宽松了,如果一个线程在临界区中的操作遇到了数据竞争,跟其它线程产生了冲突,它就会回滚这条数据,然后重试自己的操作。比如读取x与y的值,这个操作是分步进行的先读x,再读y,当读完x,发现别的线程修改了y,再读y就已经没有意义了,因为可能会读到一个错误的数据,所以该线程会重试,再去读取一次,直到自己读到的x、y没有问题为止,所以无障碍是一种会不断重试的调度策略,但它会保证没有数据竞争时,线程必然能在有限的步骤内执行完任务。

在无障碍的调度方式当中,所有的线程都相当于在拿取一个系统当前的快照,它们会一直重试,直到拿到的快照有效为止。

3、无锁(lock-free)

  • 是无障碍的
  • 保证有一个线程可以胜出

前面说的无障碍是指所有的线程都能进入临界区,但如果发生了竞争,无障碍并不保证临界区的线程能够顺利的出来,因为如果线程发现自己的数据每次去读取或者去操作,总是跟其它线程产生冲突,它就会不停地重试,如果在临界区当中有10个线程,线程1修改了部分数据,结果它被线程2干扰了,线程2又被线程3干扰,依此类推,最后线程1它又可能去干扰线程10,如果它们之间是彼此干扰的,最终会导致所有的线程都卡死在里面,系统的性能会受到比较严重的影响,因此,无锁必须在无障碍的基础上加一个约束,保证在竞争当中有一个线程是必然能够胜出的,这样就能保证在临界区的线程当中至少有一个是能顺利走出去的,而不至于全部在里面阵亡掉,如果至少有一个线程能够出去,那么就有第二个线程能够出去,假设里面有一百个线程,第一个线程竞争胜利,走出了临界区,剩下99个再竞争又必然能胜利一个,因为每次竞争它必然保证能有一个胜利,使得系统至少是能够顺畅的执行下去的,这就是无锁,下面这段代码在java当中是比较典型的使用无锁的代码:

 

while(!hyes.compareAndSet(localHyes,localHyes+1)){     localHyes = hyes.get(); }

4、无等待(wait-free)

  • 无锁的
  • 要求所有的线程都必须在有限步内完成
  • 无饥饿的

前面说了无锁是能保证至少有一个线程能够在有限步当中完成它的操作,所有的线程在不停地竞争直到有一个胜出为止。无等待相比于无锁更进一步,它首先要求是无锁的,保证所有线程能进并且至少有一个线程能出来,同时无等待它在提高要求,它要求所有进入临界区的线程都能够在有限步当中完成其操作,这个要求很高,因为任何线程都能够无障碍进入临界区,并且任何线程都能够在有限步当中完成操作后离开临界区,这就会使得整个系统的运行变得非常顺畅,无等待可以说是并行最高级别了,它基本上能使整个系统发挥到最好佳效率。

无等待必须然也是无饥饿的,因为所有的线程都能在有限步当中完成,因此必然不会有线程永久地呆在临界区内出不去,所以它一定是无饥饿的。

无等待的一个典型案例是,有读写两个线程,如果说只有读线程没有写线程,那么所有的读线程之间必然是无等待的,因为读不会修改数据,如果有一个写线程在里面,由于会修改数据 ,写线程必然会导致读线程不是无等待。因此可以提出一种算法去作一点改进,比如说有一种算法它会这样做,因为写可能会影响到读,所以每次写之前先把数据拷贝一份副本,线程修改的是这个副本而非原始数据,修改数据的过程可能需要一点时间,因为修改的是副本数据而不是原始数据,所以这个修改的过程也不影响线程读,因此在这个过程当中所有的读线程一样是无等待的,它们都能够在有限的步骤当中完成自己的操作,而所有的写线程相对来讲,因为每个写线程它都是写自己的副本,因此它们的写也是无等待的,所以它们都不需要去跟彼此作同步,最后需要同步的只是将写完之后的数据覆盖原始数据,而这个覆盖原始数据的动作是非常快的,因为我们并不需要作大量的写操作,只不过是一个指针或引用作一个替换而已,不管哪个写线程胜出,总是能够保证替换上去的数据是一致的,并不像其它的算法一样,可能会把数据写坏,因为大家都写的是副本,最后是一个指针指向谁的问题,这样数据必然是安全的,这种方式它就是无等待的一个典型的实现。

无等待的实现,在所有并发等级中是最麻烦的,而且技巧性的东西会比较多,相对来说,无锁的使用会更加广泛一些。