走进并行世界

几个概念

同步(Synchronous)和异步(Asynchronous)

同步和异步通常来形容一次方法调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续执行任务。
异步方法更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的工作。异步方法通常会在另外的线程中“真实”的执行。整个过程不会阻碍调用者的工作。

并发(concurrency)和并行(parallelism)

  • 并发
    当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式我们称之为并发(Concurrent).
  • 并行
    当系统有一个以上CPU时,则线程的操作有可能非并发.当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)
    这里写图片描述
    这里写图片描述

  • 多线程在并发和并行环境中的不同作用
    在并发环境时,多线程不可能真正充分利用CPU,节约运行时间,它只是以”挂起->执行->挂起”的方式以很小的时间片分别运行各个线程,给用户以每个线程都在运行的错觉.在这种环境中,多线程程序真正改善的是系统的响应性能和程序的友好性.
    在并行环境中, 一个时刻允许多个线程运行,这时多线程程序才真正充分利用了多CPU的处理能力, 节省了整体的运行时间.在这种环境中,多线程程序能体现出它的四大优势:充分利用CPU,节省时间,改善响应和增加程序的友好性.

临界区

临界区表示一种公共资源或者说是共享资源,可以被多个线程使用。但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想得到这个资源就必须等待。
在并行程序中。临界区资源是保护对象。就比如大家公用的一台打印机,必然是一个人打完另一个人的才能打印,否则就会出乱子。

阻塞(blocking)与非阻塞(non-blocking)

阻塞和非阻塞通常来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程都需要在临界区中等待。等待会导致线程挂起,这种情况就是阻塞。此时如果占用这个资源的线程一直不愿释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。
反之就是非阻塞,它强调没有一个线程可以妨碍其他线程执行。所有线程都会尝试不断前向执行。

死锁(deadlock)、饥饿(starvation)和活锁(livelock)

这三种情况都属于线程活跃性问题。如果发现上述情况,那么相关线程可能就不再活跃,也就是说它可能很难再继续执行任务了。

  • 死锁
    应该是最糟糕的情况之一。它们彼此相互都占用着其他线程的资源,都不愿释放,那么这种状态将永远维持下去。

  • 饥饿
    是指一个或多个线程因为种种原因一直无法得到所需要的资源,导致一直无法执行,比如它的线程优先级太低,高优先级的线程一直抢占它所需要的资源。另一种可能是某一个线程一直占用着关键资源不放,导致其他需要这个资源的线程一直无法得到这个资源,无法正常执行。与死锁相比,饥饿还是可能在一段时间内解决的,比如高优先级的线程执行完任务后,不在抢占资源,资源得到释放。

  • 活锁
    是非常有趣的情况,也是最难解决的情况。这就比如,大家在一个两人宽的桥上走路,双方都很有礼貌。都在第一时间礼让对方,一个往左一个往右,导致两人都无法正常通行。放到线程中,就体现为,两个线程都拿到资源后都主动释放给他人使用,那么就会出现资源不断的在两个线程中跳动,而没有一个线程可以拿到资源后正常执行,这个就是活锁。

并发级别

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

阻塞(blocking)

一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当我们使用synchronized关键字,或者重入锁时,我们得到的就是阻塞的线程。
无论是synchronized还是重入锁,都会在视图执行后续代码前得到临界区的锁,如果得不到,线程就会被挂起等待,直到占有了所需要的资源为止。

无饥饿

如果线程间是有优先级的,那么线程调用总是会倾向于满足高优先级的线程。也就是说对同一个资源的分配是不公平的。对于非公平的锁来说,系统允许高优先级的线程插队,这样有可能导致低优先级的线程产生饥饿。但如果锁是公平的,满足先来后到,那么饥饿就不会产生,不管新来的线程优先级多高,要想获得资源就必须排队。那么所有的线程都有机会执行。

无障碍(obstruction-Free)

无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。大家都可以大摇大摆进入临界区工作。那么如果大家都修改了共享数据怎么办呢?对于无障碍的线程来说,一旦出现这种情况,当前线程就会立即对修改的数据进行回滚,确保数据安全。但如果没有数据竞争发生,那么线程就可以顺利完成自己的工作,走出临界区。
如果阻塞控制的方式比喻成悲观策略。也就是说系统认为两个线程之间很有可能发生不幸的冲突,因此,保护共享数据为第一优先级。相对来说,非阻塞的调度就是一种乐观策略,他认为多线程之间很有可能不会发生冲突,或者说这种概率不大,但是一旦检测到冲突,就应该回滚。
从这个策略来看,无障碍的多线程程序不一定能顺利执行。因为当临界区的字眼存在严重的冲突时,所有线程可能都进行回滚操作,导致没有一个线程可以走出临界区。所以我们希望在这一堆线程中,至少可以有一个线程可以在有限时间内完成自己的操作,至少这可以保证系统不会再临界区进行无线等待。
一种可行的无障碍实现可以依赖一个“一致性标记”来实现。线程在操作之前,先读取并保持这个标记,在操作完后,再次读取,检查这个标记是否被修改过,如果前后一致,则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突,需要重试操作。任何对保护资源修改之前,都必须更新这个一致性标记,表示数据不安全。

无锁(lock-free)

无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区的资源进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。
在无锁的调度中,一个典型的特点是可能会包含一个无穷循环。在这个循环中线性不断尝试修改共享数据。如果没有冲突,修改成功,那么线程退出,否则尝试重新修改。但无论如何,无锁的并行总能保证有一个线程可以胜出,不至于全军覆没。至于临界区中竞争失败的线程,则不断重试。如果运气不好,总是不成功,则会出现类似饥饿的现象,线程会停止不前。

无等待(wait-free)

无锁是要求至少有一个线程在有限步内完成操作,而无等待则是在无锁的基础之上进一步扩展。他要求所有线程都必须在有限步内完成操作。这样就不会引起饥饿问题。如果限制这个步骤上限,还可以分为有界无等待和线程无关的无等待几种,它们之间的区别只是对循环次数的限制不同。
一种典型的无等待结构是RCU(read-copy-update)。它的基本思想是,对数据的读可以不加控制,因此所有读线程都是无等待的,它们既不会被锁定等待也不会引起任何冲突。但在写数据时,先取得原始数据的副本,接着只修改副本数据,修改完后,在合适的时机回写数据。

JMM

JMM的关键技术点都是围绕多线程的原子性、可见性与有序性来建立的。

  • 原子性
  • 可见性
    当一个线程修改了一个共享变量的值,其他线程是否能够立即知道这个修改。
  • 有序性

哪些指令不能重排

程序顺序原则:一个线程内保证语义的串行性

volatile规则:volatile变量的写,先发生与读,这保证了volatile变量的可见性。

锁规则:解锁(unlock)必然发生在随后的加锁(lock)前

锁规则强调,unlock操作必然发生在后续的对同一个锁的lock之前,也就是说,
如果对一个锁解锁后,在加锁,那么加锁的动作绝对不能重排到解锁动作之前。
很显然,如果这么做,加锁行为是无法获得这把锁的。

传递性:A先于B,B先于C,那么A必然先于C

线程的start()方法先于它的每一个动作

线程的所有操作先于线程的终结(Thread.join())

线程的中断(interrupt())先于被中断线程的代码

对象的构造函数执行、结束先于finalize()方法

猜你喜欢

转载自blog.csdn.net/demon7552003/article/details/72859751
今日推荐