并发级别
由于临界区的存在,多个线程之间的并发必须受到控制。我们把并发的级别分为:阻塞,无饥饿,无障碍,无锁和无等待。
阻塞(Blocking)
阻塞的线程在其他线程释放资源之前,无法继续执行。我们使用的synchronized和重入锁,都会在继续执行后续代码之前,尝试获得临界区的锁,如果得不到,该线程就会被挂起(阻塞),直到得到想要的资源。
无饥饿(Starvation-Free)
如果线程之间是有优先级的,那么线程的调度总是会倾向于满足高优先级的线程。换句话说,对于资源的分配是不公平的,这就涉及到公平锁和非公平锁。
公平锁就不会导致饥饿发生,就是无饥饿。
无障碍(Obstruction-Free)
无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。换言之,所有线程都可以无障碍地进入临界区。
当线程发现,有其他线程也在修改共享数据,它会立即对自己所做的修改进行回滚,确保数据安全。当然,如果没有其他线程的竞争,那么这个线程就可以顺利地完成工作。
如果说阻塞的控制方式是悲观策略,系统认为两个线程之间很有可能会发生冲突,以此,以保护共享数据作为第一优先级。相对来说,非阻塞的调度就是一种乐观的策略,它认为多个线程之间发生冲突的概率不大。因此,大家都应该无障碍地执行,但是,一旦检测到冲突,就应该进行回滚。
因此,无障碍的多线程程序不一定能顺利地运行。因为当临界区中存在严重的冲突时,所有的线程可能都会不断地回滚自己的操作,而没有一个线程能走出临界区。
一种可行的无障碍实现方式可以依赖“一致性标记”来实现。线程在操作之前,先读取并保存这个标记,在操作完成之后,再次读取,检查这个标记是否被更改过。如果两者是一致的,则说明资源访问没有冲突。如果不一致,则说明发生了冲突。
任何对资源进行修改的线程,在修改数据前,都需要更新这个一致性标记,表示数据不再安全。
无锁(Lock-Free)
无锁的并行都是无障碍的(公平锁),所有的线程都能尝试访问临界区,但与无障碍不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。
无等待(Wait-Free)
有锁只要求一个线程可以在有限步内完成操作,而无等待则在无锁的基础上,要求所有的线程都必须在有限步内完成,这样就不会引起饥饿问题了。
如果限制这个步骤上限,还可以进一步分解为有界无等待和线程数无关的无等待几种,它们之间的区别只是对循环次数的限制不同(步骤次数)。
一种典型的无等待结构就是RCU(Read-Copy-Update),它的基本思想是,对数据的读可以不加控制。因此,所有的读线程都是无等待的,它们既不会被锁定等待也不会引起任何冲突。但是在写数据的时候,先读取原始数据的副本,接着只修改副本数据,修改完成后,在适当的时机写回数据。