Java并发(一)

为什么需要并行

我们学习计算机,或者学习每一门语言的时候为什么要学习并行呢? 而且我们的计算机为什需要并行的处理机制呢? 其实这里是有原因的。原因有以下几点:

摩尔定律的失效

我们都知道,在计算机硬件行业,有著名的摩尔定律: 每18个月,芯片的性能将会提高一倍。 但是事实上并不是这样的,这个定律只是理论上的,因为在过去的10年内,我们的电脑CPU处理器的还停留在4GHZ 上,

interl.jpg-37.3kB

Intel CEO Baret在2004年的时候单膝跪地,对取消4GH
Z计划感到抱歉, 然而2004年秋季,Intell 宣布彻底取消4GHZ计划。

硬件推脱给软件

顶级计算机科学家唐纳德-尔文-克努斯说过:

在我看来,这种现象(并发)或多或少是由于硬件设计者,已经无计可施可了导致的,他们将摩尔定律失效的责任推脱给软件开发者。

为什么说硬件将摩尔定律失效推脱给了软件开发人员呢?因为硬件处理比软件处理是相对更加容易的,而且软件开发人员编写串行程序总是比编写并行程序更加容易,并行程序的编写更加复杂和困难。

并行计算还出于业务模型的需要

当然,并不是有所的原因都归结于摩尔定律的失效,还是有出于业务模型的需要,当然使用并行一是为了让提高系统性能,另一个原因是让不同的线程承担不同的业务工作,比如HTTP服务器,为每一个Socket连接建立一个处理线程,这样就可以不同的线程就可以处理不同的业务工作,这样简化了任务调度。

单个处理器的处理速度上,提升已经很难了,但是我们的却需要更快的处理速度,那么该怎么做呢?当然办法总是比问题多的。这就需要用到并行了,所以这就好为什么会出现并行的原因。

并行的几个重要概念

同步和异步

同步:也就是说,事情必须是一件一件的做,做完当前的事情,才能进行下一件事情。比如说:你是先上幼儿园,上完幼儿园之后再上小学,小学之后再上初中,事情是一件一件完成的。
异步:通俗的说就是,做一件事情时,有一部分,由于某种原因(比如你不会做),你中途叫你的朋友做,然后告诉你的朋友说,”完成之后通知我哈,我继续完成哈”,然后你就继续做你的事情去了。没有因为这件事而耽搁你的其他事情。所以这就提高了你的工作效率。

如下图:
同步异步.png-5.7kB
从上图可以知道,随着实时间的轨迹,同步一步一步的执行着,在异步中,当一个异步过程调用发出后,调用者不能立即得到结果,实际上会开启一个线程执行这部分内容,这个线程处理完了之后,通过状态,通知和回调来通知调用者来处理。

并发(Concurrency)和并行(Parallelism)

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。

并行和并发.png-4kB
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能.并行编程的目的也是达到最高的处理速率,如hadoop利用多台处理器进行并行处理。

单核CPU(单处理器)上,只可能存在并发而不可能存在并行。并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行

临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用,但是每一次,只能有一个线程使用它,一旦临界去资源被占用,其他线程想要使用这个资源,就必须等待。
临界区.png-18.4kB
这就是我们编程中经常要加锁的地方,java也就是使用使用synchronized 修饰方法或代码块。这里就不再详细做Java 中锁的介绍。

阻塞(Blocking)和非阻塞(Non-Blocking)

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

死锁、活锁、饥饿

死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

互斥条件:线程对资源的访问是排他性的,如果一个线程对占用了某资源,那么其他线程必须处于等待状态,直到资源被释放。
请求和保持条件:线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。
不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程剥夺,只能在使用完以后由自己释放。
环路等待条件:在死锁发生时,必然存在一个“进程-资源环形链”,即:{p0,p1,p2,…pn},进程p0(或线程)等待p1占用的资源,p1等待p2占用的资源,pn等待p0占用的资源。(最直观的理解是,p0等待p1占用的资源,而p1而在等待p0占用的资源,于是两个进程就相互等待

活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。

饥饿:是指如果线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求……,T2可能永远等待。

饥饿的比喻:
有两条道A和B上都堵满了车辆,其中A道堵的时间最长,B相对相对堵的时间较短,这时,前面道路已疏通,交警按照最佳分配原则,示意B道上车辆先过,B道路上过了一辆又一辆,A道上排队时间最长的确没法通过,只能等B道上没有车辆通过的时候再等交警发指令让A道依次通过,这也就是ReentrantLock显示锁里提供的不公平锁机制(当然了,ReentrantLock也提供了公平锁的机制,由用户根据具体的使用场景而决定到底使用哪种锁策略),不公平锁能够提高吞吐量但不可避免的会造成某些线程的饥饿。

并发级别

  • 阻塞:当一个线程进入临界区后,其他线程必须等待
  • 无障碍:
    无障碍是一种最弱的非阻塞调度
    自由出入临界区
    无竞争时,有限步内完成操作
    有竞争时,回滚数据
    
  • 无锁
    是无障碍的
    保证有一个线程可以胜出

    1
    2
    3
    4
    while (!atomicVar.compareAndSet(localVar, localVar+1))
    {
    localVar = atomicVar.get();
    }
  • 无等待
    无锁的
    要求所有的线程都必须在有限步内完成
    无饥饿的

其中无障碍,无锁、无等待是非阻塞的。

关于并行的2个重要定律

Amdahl定律(阿姆达尔定律)

定义了串行系统并行化后的加速比的计算公式和理论上限
加速比定义:加速比=优化前系统耗时/优化后系统耗时

阿姆达尔定律定义
一个程序(或者一个算法)可以按照是否可以被并行化分为下面两个部分:

  • 可以被并行化的部分
  • 不可以被并行化的部

假设一个程序处理磁盘上的文件。这个程序的一小部分用来扫描路径和在内存中创建文件目录。做完这些后,每个文件交个一个单独的线程去处理。扫描路径和创建文件目录的部分不可以被并行化,不过处理文件的过程可以。
阿姆达尔定律.png-15.7kB
加速比=优化前系统耗时/优化后系统耗时=500/400=1.25
amd.png-35kB

当然,上面的这个计算方法可以用这个上诉的公式得出相同的结果。

增加CPU处理器的数量并不一定能起到有效的作用
提高系统内可并行化的模块比重,合理增加并行处
理器数量,才能以最小的投入,得到最大的加速比

Gustafson定律(古斯塔夫森)

古斯塔夫森:说明处理器个数,串行比例和加速比之间的关系
Gustafson.png-20.8kB

只要有足够的并行化,那么加速比和CPU个数成正比

坚持原创技术分享,您的支持将鼓励我继续创作!