一、何去何从的并行

不多哔哔,直接进入正题

1、概念

1.1 同步和异步

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

打个比方,比如你找个同学带你上荣耀,你就要和他一起打,直到他带你上了荣耀为止你才会结束,这就是同步。但是你找个代练,你把钱付了剩下就不用管了,代练会自己帮你打。自己可以该干什么干什么,这就是异步。

1.2 并发和并行

并发和并行是两个非常容易被混淆的概念。他们都可以表示多个任务一起执行,但是侧重点不同。并发偏重于多个任务交替执行,意思就是任务一执行一部分再执行任务二再执行任务三,轮流执行,其实是串联。而并行是真正意义上的”同时执行“。对于外部观察者来说,会执行并发也是一起执行的错觉。

1.3 临界区

临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用。但每一次只有一个线程可以使用它,一旦临界区资源被占用,其他线程只能挂起。比如办公室的打印机,一次只能执行一个任务。

在并行程序中,临界区资源是保护的对象,如果出现意外,打印机同时打印两个任务,那么最有可能的结果就是打印出来的文件是坏的。

1.4 阻塞和非阻塞

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

非阻塞的意思与阻塞相反,它强调没有一个线程可以妨碍其他线程执行,所有的线程都会不断向前执行。有关这个概念在后面”并发级别“中会详细介绍

1.5 死锁、饥饿、活锁

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

① 死锁是最糟糕的情况,死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去;此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

② 饥饿是指系统不能保证某个进程的等待时间上界,从而使该进程长时间等待,当等待时间给进程推进和响应带来明显影响时,称发生了进程饥饿。当饥饿到一定程度的进程所赋予的任务即使完成也不再具有实际意义时称该进程被饿死。

③ 活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动”

简单来说,死锁就是我们两个都要做蛋炒饭,你想要我手里的火腿肠,但是我想不给。我想要你手里的鸡蛋,你也不想给,然后我俩的蛋炒饭都执行不下去就一直耗着。饥饿就是我答应给你一个火腿肠让你去炒饭吃,但是没说啥时候给你,就一直让你等,等到最后直接饿死。活锁就是我们都有自己需要的食材但是又互相送,不想炒。

2、并发级别

2.1 阻塞

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

synchronized关键字和重入锁都试图在执行后续代码前,得到临界区的资源,如果得不到,线程就会被挂起等待,直到获得资源为止

2.2 无饥饿

如果线程之间有优先级,那么线程调度的时候总是会先满足优先级高的线程。也就是说,对于同一个资源的分别配是不公平的!公平锁和非公平锁两种情况,对于非公平锁就是说谁优先级高就执行谁,这样有可能会让优先级低的线程饿死。对于公平锁就是谁先来谁先执行按顺序,不畏权贵。

2.3 无障碍

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

2.4 无锁

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

2.5 无等待

无等待是在无锁的基础上进行优化,它要求所有的线程都必须在有限的步内完成,这样就不会引起饥饿的问题。

3、有关并行的两个重要定律

有关为什么要使用并行程序的问题前面已经进行了简单的探讨。总的来说,最重要的应该是出于两个目的。第一,为了获得更好的性能;第二,由于业务模型的需要,确实需要多个执行实体。在这里,我将更加关注于第一种情况,也就是有关性能的问题。将串行程序改造为并发程序,一般来说可以提高程序的整体性能,但是究竟能提高多少,甚至说究竟是否真的可以提高,还是一个需要研究的问题。目前,主要有两个定律对这个问题进行解答,一个是Amdahl定律,另外一个是Gustafson定律。

3.1 Amdahl定律

Amdahl定律是计算机科学中非常重要的定律。它定义了串行系统并行化后的加速比的计算公式和理论上限。

(JAVA高并发程序设计)第一章、走进并行世界_死锁

(JAVA高并发程序设计)第一章、走进并行世界_开发语言_02

(JAVA高并发程序设计)第一章、走进并行世界_后端_03

3.2 Gustafson定律

(JAVA高并发程序设计)第一章、走进并行世界_死锁_04

可以看到,由于切入角度的不同,Gustafson定律的公式和Amdahl定律的公式截然不同。从.Gustafson定律中,我们可以更容易地发现,如果串行化比例很小,并行化比例很大,那么加速比就是处理器的个数。只要不断地累加处理器,就能获得更快的速度。

3.3 是否相互矛盾

(JAVA高并发程序设计)第一章、走进并行世界_开发语言_05

(JAVA高并发程序设计)第一章、走进并行世界_开发语言_06

4、回到JAVA:jmm

4.1 原子性

原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

.比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A给它赋值1,线程B给它赋值为-1。那么不管这两个线程以何种方式、何种步调工作,i的值要么是1,要么是-1。线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断。

但如果我们不使用int 型数据而使用long型数据,可能就没有那么幸运了。对于32位系统来说,long型数据的读写不是原子性的(因为long型数据有64位)。也就是说,如果两个线程同时对long 型数据进行写入(或者读取),则对线程之间的结果是有干扰的。

大家可以仔细观察一下下面的代码:

(JAVA高并发程序设计)第一章、走进并行世界_临界区_07

上述代码有4个线程对long 型数据t进行赋值,分别对t赋值为111、-999、333、444.然后,有一个读取线程读取这个t的值。一般来说,t的值总是这4个数值中的一个。这当

(JAVA高并发程序设计)第一章、走进并行世界_死锁_08

4.2 可见性

可见性是指当一个线程修改了某一个共享变量的值时,其他线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题是不存在的。因为你在任何一个操作步骤中修改了某个变量,在后续的步骤中读取这个变量的值时,读取的一定是修改后的新值。

4.3 有序性

有序性问题可能是三个问题中最难理解的了。对于一个线程的执行代码而言,我们总是习惯性地认为代码是从前往后依次执行的。这么理解也不能说完全错误,因为就一个线程内而言,确实会表现成这样。但是,在并发时,程序的执行可能就会出现乱序。给人的直观感觉就是:写在前面的代码,会在后面执行。听起来有些不可思议,是吗?有序性问题的原因是程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。