并发程序设计01——基本概念

来源:互联网 发布:java图形用户界面设计 编辑:程序博客网 时间:2024/06/15 19:41

基本概念

同步和异步

同步和异步通常用来形容一次方法调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。而异步方法通常会在另外一个县城中“真实”地执行。整个过程不会阻碍调用者的工作。

并行和并发

并行的多个任务是真实的同事执行,而对于并发来说,这个过程只是交替的。一会运行任务A一会执行任务B,系统会不停地在两者间切换。

临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。

阻塞和非阻塞

阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个县城占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。

非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行。所有的线程都会尝试不断向前执行。

死锁、饥饿和活锁

死锁、饥饿和活锁都属于多线程的活跃性问题。如果发现上述几种情况,那么相关线程可以就不能活跃,也就说它可能很难再继续往下执行了。

死锁,互相持有多方的锁,程序无法继续执行!死锁是一个很严重的,并且应该避免和时时小心的问题。

饥饿时指某一个或多个线程因为种种原因无法获得所需的资源,导致一直无法执行。比如它的线程优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级的线程无法工作。另外一种可能是,某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。与死锁相比,饥饿还是有可能在未来一段时间内解决的(比如高优先级的线程已经完成任务)。

活锁是一种有趣的情况。有这样一种场景,当你要坐电梯下楼,电梯到了,门开了,这是你正准备出去。但很不巧的是,门外一个人挡着你的去路,他想进来。于是你很绅士地靠左走,避让对方。同时对方也很绅士地,但他靠右走希望比让你。于是结果你两就又撞上了……一直避让、一直撞上。

如果两个线程发生了这种情况,都秉持着谦让的原则,主动将资源释放给他人使用,那么就会出现资源不断在这两个线程中跳动,而没有一个线程可以同时拿到所有资源而正常执行。这种情况就是活锁。

并发级别

由于临界区的存在,多线程之间的额并发级别必须受控制。根据控制并发的策略,我们可以把并发的几倍进行分类,大致上可以分为阻塞、无饥饿、无障碍、无锁、无等待几种。

阻塞

一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当我们使用synchronized关键字,或者重入锁,我们得到的就是阻塞的线程。

无饥饿

如果线程之间是有优先级的,那么线程调度的时候总是会倾向于满足搞优先级的线程。也就是说,对于同一个资源的分配是不公平的!对于非公平的锁来说,系统允许高优先级的线程插队。这样有机会导致低优先级的线程产生饥饿。但如果锁是公平的,满足先来后到,那么久不会产生饥饿。

无障碍

无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。换言之,大家都可以进入临界区。那么如果大家一个修改共享数据,把数据改坏了怎么办?对于无障碍的线程来说,一旦检测到这种情况,它就会立即对自己所有的修改进行回滚,确保数据的安全。如果没有数据竞争发生,那么线程就可以顺利完成自己的工作,走出临界区。

如果说阻塞的控制方式是一种悲观策略。相对来说,非阻塞的调度就是一种乐观的策略。

从这个策略中也可以看出,无障碍的多线程程序并不一定能顺利的运行。因为当临界区中存在严重的冲突时,素有的线程可能都会不断地回滚自己的操作,而没有一个程序可以走出临界区。

一种可行的无障碍实现可以依赖一个“一致性标记”来实现。程序在操作之前,先读取并保存这个标记,在操作完成后,再次读取,检查这个标记是否被更改过,如果两者一致的,则说明资源没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突,需要重试操作。

无锁

无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。

在无锁的调用中,一个典型的特点是可能会包含一个无穷循环。在这个循环中,线程会不断尝试修改共享变量。如果没有冲突,修改成功,那么程序退出,否则继续尝试修改。但无论如何,无锁的并行总能保证有一个线程是可以胜出的至于临界区中竞争失败的线程,他们则必须不断重试,直到自己获胜。如果运气不好,总是尝试不成功,则会出现类似饥饿的现象,程序会停止不前

无等待

无锁只要求有一个线程可以在有限次内完成操作,而无等待则在无锁的基础上更进一步扩展。他要求所有线程都必须在有限步内完成,这样就不会引起饥饿的问题。如果限制步骤上限,还可以进一步分解为有界无等待和线程数无关的无等待几种,他们之间的区别只是对循环次数的限制不同。

一种典型的无等待结构就是RCU(Read-Copy-Update)。它的基本思路就是,对数据的读可以不加以控制。因此所有的读线程都是无等待的,他们既不会被锁定等待也不会引起任何冲突。但在写数据的时候,先读取原始数据的副本,接着只修改副本数据,修改完成后,在合适的时机写数据。