由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,我们可以把并发的级别分为阻塞、无饥饿、无障碍、无锁、无等待几种。
阻塞
个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当我们使用synchronized关键字或者重入锁时,我们得到的就是阻塞的线程。
synchronize关键字和重入锁都试图在执行后续代码前,得到临界区的锁,如果得不到,线程就会被挂起等待,直到占有了所需资源为止.
总结:
当一个线程进入临界区(公共资源区)后,其他线程必须在临界区外等待,待进去的线程执行完成任务离开临界去后,其它线程才能进去。
无饥饿(Starvation-Free)
如果线程之间是有优先级的,那么线程调度的时候总是会倾向于先满足高优先级的线程
。也就是说,对于同一个资源的分配,是不公平的.
对于非公平锁来说
,系统允许高优先级的线程插队。这样有可能导致低优先级线程产生饥饿
。但如果锁是公平的
,按照先来后到的规则,那么饥饿就不会产生,不管新来的线程优先级多高,要想获得资源,就必须乖乖排队,这样所有的线程都有机会执行
.
总结:
线程排队先来后到,不管优先级大小,先来先执行,,就不会产生饥饿等待资源,也即公平锁;相反非公平锁则是根据优先级来执行,有可能排在前面的低优先级线程被后面的高优先级插队,形成饥饿。
无障碍(Obstruction-Free)
无障碍是一种最弱的非阻塞调度
。两个线程如果无障碍地执行,那么不会因为临界区的问题导致一方被挂起,换言之,大家都可以大摇大摆地进入临界区了。那么大家一起修改共享数据,把数据改坏了怎么办呢?对于无障碍的线程来说,一旦检测到这种情况,它就会立即对自己所做的修改进行回滚,确保数据安全。但如果没有数据竞争发生,那么线程就可以顺利完成自己的工作,走出临界区
.
- 阻塞的控制方式是悲观策略
- 非阻塞的调度就是一种乐观的策略
从这个策略中也可以看到,无障碍的多线程程序并不一定能顺畅运行
。因为当临界区中存在严重的冲突时,所有的线程可能都会不断地回滚自己的操作,而没有一个线程可以走出临界区。这种情况会影响系统的正常执行。所以,我们可能会非常希望在这一堆线程中,至少可以有一个线程能够在有限的时间内完成自己的操作,而退出临界区。至少这样可以保证系统不会在临界区中进行无限的等待。
一种可行的无障碍实现
可以依赖一个"一致性标记"来实现。线程在操作之前,先读取并保存这个标记,在操作完成后,再次读取,检查这个标记是否被更改过,如果两者是一致的,则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他线程冲突,需要重试操作。而任何对资源有修改操作的线程,在修改数据前,都需要更新这个一致性标记,表示数据不再安全.
例如:数据库中乐观锁,表中需要一个字段version(版本号),每次更新数据version+1,更新的时候将版本号作为条件进行更新,根据更新影响的行数判断更新是否成功
总结:
共享资源不加锁,每个线程都可以自由读写,当监测到被其他线程修改过,则回滚操作,重试直到单独操作成功;当多个线程同时读写,容易造成死循环。
无锁(Lock-Free)
无锁的并行都是无障碍的
。在无锁的情况下,所有的线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区
。
总结:
无锁是无障碍的加强版,无锁级别保证至少有一个线程在有限操作步骤内成功退出,不管是否修改成功,这样保证了多个线程回滚不至于导致死循环。
无等待
无锁只要求有一个线程可以在有限步内完成操作
,而无等待则在无锁的基础上更进一步扩展。它要求所有线程都必须在有限步内完成
,这样不会引起饥饿问题.
总结:
无等待是无锁的升级版,
并发编程的最高境界
,无锁只保证有线程能成功退出,但存在低级别的线程一直处于饥饿状态,无等待则要求所有线程必须在有限步骤内完成退出,让低级别的线程有机会执行,从而保证所有线程都能运行,提高并发度。