并发与并行的定义:

并发与并行,是两个完全不一样的概念,教科书中对其的定义如下:
并发:事件在一段时间内同时发生。
并行:事件在同一刻发生。

通过定义可以看出,并发,是广义上的并行,并行,是狭义上的并发,比如A同学和B同学在10分钟内都把作业做完了,如果这10分钟相对起来是一个非常非常小的时间单位,那么称A 和B是并发,如果A 同学和B同学在枪响的同一刻起跑了,那么 A与B同学是并行。

并发的诞生:

在dos时代,著名的dos操作系统,是单用户单任务,单线程,意味着在同一时刻,只能有一个任务被cpu执行,如果有其他任务,那么必须等待前一个任务执行完毕,这个时代没有任何的并发,虽然曾经有款病毒,可以在dos下蹦出一个乒乓球在屏幕跳,并且不影响当前任务执行,但是该病毒利用的是cpu中断机制。

dos系统逐渐淡出市场,被多任务系统取代,1985年的amiga是首个多任务操作系统,但是它的多任务是基于抢占式,直到win1.0的出现,才实现了真正的多任务操作系统,基于时间片轮询,也就是正式的并发时代开启。
所谓的时间片,即cpu分配给程序执行的时间,使各个程序表面上看起来是一起在执行,例如当一个用户在听歌的同时还在上网,从cpu的角度来讲,是执行听歌的任务x秒,再执行上网的任务x秒,只是这个x秒,足够小,凭人的感知无法感知到间断,这个机制和目前的动态扫描数码管机制是一样的,例如:

001

虽然看起来在屏幕上显示了12345678的一串数字,实际上是通过段选和位选,先显示了1,再显示2,再显示3以此显示,只是这个间隔时间足够短,肉眼无法识别,于是看起来就是显示了一串数字,如果延长这个时间片的时间,就能够明显的看到是8个数字跳动显示,这也是典型的时间片机制。

通过时间片,可以在广义上看起来让两个任务同时进行,这样大大的提高了cpu的利用率,之所以说是大大的提高,原因在于一个程序员通常并不是全程的cpu计算,还需要外围设备,例如告诉数据总线去内存加载数据,使用io从硬盘加载数据,这些时间,对于cpu来说,是漫长的等待,而采用时间片,则可以在这段等待的时间,继续执行其他的任务,极大的提高了效率。

从jdk1.0开始,java就提供了针对并发编程的框架,就是熟知的Thread 类和Runnable接口,使用子线程编程,只需要考虑业务层的同步,因为每个cpu时间片的切换,cpu在锁存器中会保存之前的状态,以便下一次加载。因此通过继承Thread或者实现Runnable接口,就能够简单的将多个任务切分成一个小单元,并发的执行。

002

随着计算机的不断发展,cpu的计算能力不断提高,为了充分利用cpu的能力,所有的编程语言也都在不停的改变着,因为cpu的伪并发,会造成很多无意义的调度,而每一次调度,都会造成很大的系统开销,另外在很多时候需要做很多无意义的线程同步和线程通信,于是很多语言相继实现了另一个模型:coroutine,协程,也成为用户形态的轻量级线程,它可以避免cpu无意义的调度所造成的资源浪费,带来的代价就是程序员需要自己承担调度的责任。

java并没有提供 coroutine,不过目前有第三方库,例如kilim,但是在jdk5中,java引入了excutor,将线程保留在池中,减少了创建线程所带来的开销。

003

并行的出现

随着计算机的发展,单个cpu的利用已经被压榨到了极限,于是出现了多个cpu,也就是多核的时代,多核的编程和单核的编程几乎变得不太一样了,单核中,是没有并行的,因此单核中不用考虑cpu级别的问题,但是在多核下,就不一样了,第一颗核计算的数据,很大可能被第二颗核的任务给修改了,虽然这样并不会带来结果的改变,因为cpu内置机制保证了结果一致,但是却对性能造成了巨大的伤害。一个多颗cpu的系统结构如下所示:

004

可以看到,每个cpu有自己的cache,其中L1并不是一块,而是两块,分为代码缓存和数据缓存,而三级缓存,是所有核共享的,因此很容易出现cpu伪共享,这些问题也导致了很多代码,一旦放到多核上,性能不但没有提升,反而降低了不少。

在java7中,引入了ForkJoin框架,将一个任务,拆分成多个子任务,放到不同的核上面去执行,最终再合并,这个思路看起来吊炸天了,和有个东西太像了,嗯,就是map reduce,例如需要计算1+2+3+4+5+6

通过forkjoin,则会将其拆分成(1+2)(3+4) (5+6)放到不同的核上去计算,最终再将结果合并。虽然这样的并行框架提供了多核的计算,但是ForkJoin仅仅提供了执行框架,而没有保证性能问题,程序员需要为多核下的性能负责,例如如下代码:

005
006

这两段代码,看起来几乎没有啥区别,唯独第二段代码的pojo类,定义了一些根本没有用的变量,但是如果我们使用4个线程,第一个线程修改数组的第一个元素,第二个线程修改第二个元素,以此类推,我们会发现,第一段代码的性能会比第二段的代码差3倍,而他们的区别,仅仅就是第二段代码中,定义了一些尚未使用的变量,而正是这些并没有使用的变量,让代码的性能,瞬间提升了几个档次。其原因就在于longs数组的4个元素,由于VolattileLong只有一个长整型的成员,所以整个数组都将被加载至同一行缓存行,但有4个线程同时操作这条缓存行,于是伪共享就悄悄发生了。而定义的这些并没有使用的对象,将每个对象占用有个缓存行,就避免了这类情况。


扫码手机观看或分享: