由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,可以把并发分为几个级别:阻塞、无饥饿、无障碍、无锁、无等待。
当一个线程等待由其他线程占有的资源,并且在资源被释放之前当前线程无法继续执行,这时我们称这个线程为“ 阻塞 ”的。当我们使用synchronized关键字或重入锁时(这两种技术将在后续文章中介绍),得到的就是阻塞的线程。
阻塞的线程在执行后续代码前会尝试得到临界区的锁,如果尝试失败,线程将会被挂起处于等待状态,直到抢占到所需资源为止。
线程是有优先级的区分的,当线程调度的时候总会倾向于优先将资源分配给高优先级的线程。也就是说,对同一个资源的分配是不公平的。下图显示了非公平与公平两种情况。
对非公平锁来说,系统允许高优先级的线程插到低优先级线程之前执行,这就可能会导致低优先级线程一直得不到执行,即产生了 饥饿 。但如果是公平锁,系统将会按先来后到的顺序依次执行线程,不管线程优先级有多高都必须排队,这样所有的线程都有机会执行,也就是 无饥饿。
无障碍是一种最弱的 非阻塞 调度,线程之间如果是无障碍执行的话,那么他们不会因为临界区的问题导致被挂起。在无障碍的情况下,为了保证临界区中的共享数据不被破坏,线程需要检测共享数据是否完整,如果数据被破坏,线程就会对自己所做的修改进行回滚,确保数据安全。
阻塞的控制方式属于悲观策略,即系统认为多个线程之间很有可能对资源发生争抢,因此选择保护共享数据为第一优先级。反观非阻塞调度就属于乐观策略,系统认为多个线程之间发生冲突的可能性很小,因此多线程应该无障碍的执行,当检测到冲突时再进行回滚。
但是当临界区存在严重的冲突时,可能会发生所有线程不断回滚自己的操作,而没有一个线程可以走出临界区,这种情况会影响系统的正常执行。
在上述无障碍的场景中,有可能发生线程无法退出临界区的情况,而 无锁 就是一种无障碍策略的实现,在无障碍的基础上添加了一个条件:保证必然有一个线程能够在有限步内完成操作离开临界区。
在无锁的调用中,一个典型的特点是可能会包含一个无限循环。在循环中线程会不断尝试修改共享变量。如果没有冲突则修改成功,线程走出临界区;如果遇到冲突则重试修改操作。但无论如何,无锁算法总能保证有一个线程可以成功。
Java中提供了一种无锁算法:Compare-And-Swap(CAS),由于计算机在CPU层面上支持CAS的原子操作,所以当多线程争相修改共享资源时必然会有一个线程修改成功。但当竞争非常激烈时,某些“运气不佳”的线程也会出现饥饿的情况。
while (!atomicVar.compareAndSet(localVar, localVar+1)) { localVar = atomicVar.get(); } 复制代码
无锁只要求有一个线程可以在有限步内完成操作,而无等待则是在无锁的基础上进一步扩展:它要求所有的线程都在有限步内完成操作。这样就不会引发饥饿问题。
一种典型的无等待结构就是RCU(Read-Copy-Update)。它的基本思想是对数据的读可以不加控制。因此所有的读线程都是无等待的。当写数据时,先取得原始数据的副本,接着只修改副本数据,等待合适的时机将副本数据回写到原始数据。