一般认为并发可以分为阻塞与非阻塞,对于非阻塞可以进一步细分为无障碍、无锁、无等待,下面就对这几个并发级别,作一些简单的介绍。
并发级别
1、阻塞
阻塞是指一个线程进入临界区后,其它线程就必须在临界区外等待,待进去的线程执行完任务离开临界区后,其它线程才能再进去。
2、无障碍(obstruction-free)
无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。换言之,大家都进入临界区了。那么如果一起修改共享数据,把数据改坏了可怎么办呢?对于无障碍的线程来说,一旦检测到这种情况,它就会立即对自己所做的修改进行回滚,确保数据安全。从这个策略中也可以看到,当临界区中存在严重的冲突时,所有的线程可能都会不断地回滚自己的操作,而没有一个线程可以走出临界区。这种情况会影响系统的正常执行。
一种可行的无障碍实现可以依赖一个“一致性标记”来实现。线程在操作之前,先读取并保存这个标记,在操作完成后,再次读取,检查这个标记是否被更改过,如果两者是一致的,则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突,需要重试操作。而任何对资源有修改操作的线程,在修改数据前,都需要更新这个一致性标记,表示数据不再安全。
跟非阻塞调度比较,阻塞调度可以认为是一种悲观的策略,它会认为多个线程一起修改数据会使数据损坏,所以阻塞调度每次只能允许一个线程去修改数据。而非阻塞调度相对来说比较乐观,它认为如果多个线程一起修改也未必会把造成数据损坏,所以它允许多个线程同时进入临界区,但无障碍是一种宽进严出的策略,进的时候不作限制,所有的线程都能进入临界区做其想做的事情,包括读与写,但是出来的时候就不那么宽松了,如果一个线程在临界区中的操作遇到了数据竞争,跟其它线程产生了冲突,它就会回滚这条数据,然后重试自己的操作。比如读取x与y的值,这个操作是分步进行的先读x,再读y,当读完x,发现别的线程修改了x,再读y就已经没有意义了,因为可能会读到一个错误的数据,所以该线程会重试,再去读取一次,直到自己读到的x、y没有问题为止,所以无障碍是一种会不断重试的调度策略,但它会保证没有数据竞争时,线程必然能在有限的步骤内执行完任务。
在无障碍的调度方式当中,所有的线程都相当于在拿取一个系统当前的快照,它们会一直重试,直到拿到的快照有效为止。
3、无锁(lock-free)
是无障碍的
保证有一个线程可以胜出
前面说的无障碍是指所有的线程都能进入临界区,但如果发生了竞争,无障碍并不保证临界区的线程能够顺利的出来,因为如果线程发现自己的数据每次去读取或者去操作,总是跟其它线程产生冲突,它就会不停地重试,如果在临界区当中有10个线程,线程1修改了部分数据,结果它被线程2干扰了,线程2又被线程3干扰,依此类推,最后线程1它又可能去干扰线程10,如果它们之间是彼此干扰的,最终会导致所有的线程都卡死在里面,系统的性能会受到比较严重的影响,因此,无锁必须在无障碍的基础上加一个约束,保证在竞争当中有一个线程是必然能够胜出的,这样就能保证在临界区的线程当中至少有一个是能顺利走出去的,而不至于全部在里面阵亡掉,如果至少有一个线程能够出去,那么就有第二个线程能够出去,假设里面有一百个线程,第一个线程竞争胜利,走出了临界区,剩下99个再竞争又必然能胜利一个,因为每次竞争它必然保证能有一个胜利,使得系统至少是能够顺畅的执行下去的,这就是无锁,下面这段代码在java当中是比较典型的使用无锁的代码:
while(!hyes.compareAndSet(localHyes,localHyes+1)){
localHyes = hyes.get();
}
在高并发多线程中,CAS(Compare And Swap,比较交换)技术就是一种无锁实现.在它的实现中,使用了一个无限循环,当要修改的内容和期望内容一致时,才去做修改.因此,CAS对死锁是免疫的.在java.util.concurrent.atomic包下(在jdk的rt.jar中)的各种原子类实现,都使用了CAS技术.例如在AtomicInteger中的getAndSet(int newValue)方法.
另外,使用无锁方式,省去了线程之间竞争临界区资源锁而产生的性能损耗,也没有线程之间频繁调度带来的开销.
4、无等待(wait-free)
无锁的
要求所有的线程都必须在有限步内完成
无饥饿的
前面说了无锁是能保证至少有一个线程能够在有限步当中完成它的操作,所有的线程在不停地竞争直到有一个胜出为止。无等待相比于无锁更进一步,它首先要求是无锁的,保证所有线程能进并且至少有一个线程能出来,同时无等待它在提高要求,它要求所有进入临界区的线程都能够在有限步当中完成其操作,这个要求很高,因为任何线程都能够无障碍进入临界区,并且任何线程都能够在有限步当中完成操作后离开临界区,这就会使得整个系统的运行变得非常顺畅,无等待可以说是并行最高级别了,它基本上能使整个系统发挥到最好佳效率。
无等待必须然也是无饥饿的,因为所有的线程都能在有限步当中完成,因此必然不会有线程永久地呆在临界区内出不去,所以它一定是无饥饿的。
无等待的一个典型案例是,有读写两个线程,如果说只有读线程没有写线程,那么所有的读线程之间必然是无等待的,因为读不会修改数据,如果有一个写线程在里面,由于会修改数据 ,写线程必然会导致读线程不是无等待。因此可以提出一种算法去作一点改进,比如说有一种算法它会这样做,因为写可能会影响到读,所以每次写之前先把数据拷贝一份副本,线程修改的是这个副本而非原始数据,修改数据的过程可能需要一点时间,因为修改的是副本数据而不是原始数据,所以这个修改的过程也不影响线程读,因此在这个过程当中所有的读线程一样是无等待的,它们都能够在有限的步骤当中完成自己的操作,而所有的写线程相对来讲,因为每个写线程它都是写自己的副本,因此它们的写也是无等待的,所以它们都不需要去跟彼此作同步,最后需要同步的只是将写完之后的数据覆盖原始数据,而这个覆盖原始数据的动作是非常快的,因为我们并不需要作大量的写操作,只不过是一个指针或引用作一个替换而已,不管哪个写线程胜出,总是能够保证替换上去的数据是一致的,并不像其它的算法一样,可能会把数据写坏,因为大家都写的是副本,最后是一个指针指向谁的问题,这样数据必然是安全的,这种方式它就是无等待的一个典型的实现。
无等待表示任何线程都可以在有限步骤内结束,而不必关心其他线程进度如何.
进一步分类可以分为有界无等待Wait-Free Bounded (WFB)和集居数无关无等待Wait-Free Population Oblivious.
有界无等待:按照英文愿意,是指方法的执行过程都可以在有界限的步骤内完成,但是这个过程可能是与线程数量相关的.
集居数无关无等待(也可以叫做线程数无关无等待):在英文文献中,是这么说的–一个无等待的方法,如果其性能和活动线程数目无关,那么被称为集居数无关无等待的。
Wait-free bounded(有界无等待):
如果所有的L个线程消耗C(N,L)或者更少的时间完成操作:OpsF() < C(N,L)
Wait-free population oblivious(集居数无关无等待,在并发变成实战中翻译成了线程数无关无等待,也准确):
如果所有的L个线程在有限操作内完成F,并且和L无关:OpsF() < C(N).其中,设F为一个函数方法,设L为同时调用F的并发线程数目,设N为一个与L无关的变量,设OpsF()代表一个指定线程完成F需要进行的操作步骤,设C(N,L)为一个依赖N和L的函数.
一种典型的无等待结构就是Read-Copy-Update(RCU).它的基本思想是,对数据的读可以不加控制,因此,所有的读线程都是无等待的.但是在写数据的时候,需要先取得原始数据的副本,接着修改副本数据,修改完成后,然后在合适的时机回写数据.
无等待的实现,在所有并发等级中是最麻烦的,而且技巧性的东西会比较多,相对来说,无锁的使用会更加广泛一些。