Java并发(1、概述)

离上次写博客又隔了好久,心中有愧。在我不断使用Java的过程当中,几乎都是拿来就用,就Java并发这块我尚未系统的梳理过,趁着国庆有空余时间,把它梳理一遍。如下部份内容参考相关书籍,以做学习之用,特此说明。java

1.并行定律

随着科技的发展,集成电路上的晶体管数量也达到了物理极限,摩尔定律也随之再也不那么有效,例如Amdahl定律和Gustafson定律代替它成为计算机性能发展的源动力。从这个演变也能够看出,计算机的性能发展也不得不从追求处理器频率到多核并行处理的发展过程。数组

1.1.定义

  所谓阿姆达尔(Amdahl)定律,它定义了串行系统并行化后的加速比的计算公式和理论上限。缓存

1.2.公式

  就是其公式就是:

  其中Sp就是加速比,T1是优化前系统耗时,Tp是优化以后系统耗时,p就是处理器个数。那么这个公式意义就是 加速比 = 优化前系统耗时 / 优化后系统耗时。
  咱们逐步看一下它的公式推导:

  其中,p为处理器个数,F为串行比率,那么1-F就是并行的比例了。这个公式就是计算优化后的耗时公式,将这个公式代入加速比公式咱们就能够得出CPU的处理器数量越多,那么加速比与系统的串行率就成反比:

  咱们不妨看个例子,假设如今有个系统是按以下方式串行运行的:
并发

  这个系统有三步,其中第一步和第三步都是100ms,第二步是200ms,整个串行的运行时间是400ms。那咱们如今可能要对这个系统作个优化,已知这个系统是两个核心,那么若是Step2的操做内部由串行改成并行,那么理想状况多是这样的:
app

  咱们看到Step2分解成并行的操做,那么代入公式获得最终它的加速比为1.2。咱们不妨推算一下,假设处理器的个数为无穷大,那么Step2的操做耗时无限趋近于0,那么对于这个系统而言,它的加速比(300ms/200ms)最大也不过是1.5。也就是说,P越趋近于无穷大,那么Sp=1/F。
  加速比越高,代表优化效果越好。根据Amdahl定律,使用多核的CPU对系统优化,优化的效果取决于CPU的数量和系统串行化程序的比重,若是仅仅提高Cpu数量,而不下降程序的串行比重,也是没法提升系统性能的。因此,咱们要根本上去改变程序的串行行为,合理的并行与增长处理器数量,才能得到更大的性能提高。性能

1.3.Gustafson定律

   Gustafson定律只是从不一样的角度去阐述处理器个数、串行比例和加速比之间的关系。因此这里再也不赘述。学习

2.Java内存模型

2.1.处理器、高速缓存、主存交互

   提升计算机的性能并非让计算机同时处理多个任务那样简单,处理器须要和内存交互,例如读取数据、存储运算结果,由于现代计算机的处理器能力太强,存储设备的读写速度与之相差太大,因此在存储设备和处理器之间加上高速缓存来做为处理器和内存之间的缓冲。这样的话CPU就不须要等待相对而言缓慢的内存读写了。
   当高速缓存做为一种解决处理器与内存读写速度矛盾的手段时,带来了新的问题,那就是缓存一致性。处理器有对应的高速缓存,而它们又对应同一块主内存。当多个处理器的运算都涉及到同一个主内存时,该如何保证数据的一致性?因此为了解决一致性,又在处理器访问缓存时候遵循一些协议。
   那么诸如Java虚拟机的内存模型之类就能够理解成,在特定的操做协议下,对特定的内存或者高速缓存进行读写的过程抽象。
   除了高速缓存以外,处理器也会对输入代码乱序执行优化(Out-Of-Order Execution)优化,这种优化并不能保证处理器的执行顺序会和输入代码的顺序一致,但会保证最终输入的结果是一致的。与之相对应的,Java也存在着一套相似的机制,就是指令重排(Instruction Reorder)优化。
优化

2.2.Java内存模型(JMM)

   Java虚拟机定义了一套内存模型来屏蔽各类硬件和操做系统带来的内存访问差别,实现Java程序在各平台下达到一致的内存访问结果。
   JMM主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储、取出的底层细节。这里的变量包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,由于这些是线程私有的,不会被共享。
   Java内存模型规定全部变量都存在主内存,每条线程都有本身的工做内存,线程全部对变量的操做都必须在工做内存中执行,线程的工做内存保存了被该线程使用到的变量主内存拷贝,不能直接读写主内存中的变量,线程之间变量值传递须要经过主内存完成。线程、主内存、工做内存交互以下:
this

2.3.内存间的交互操做

   在主内存和工做内存之间的交互协议的具体细节,Java内存模型定义了8个操做来完成,虚拟机来保证这8个操做都是原子的。atom

操做 说明 描述
lock 锁定 做用于主内存的变量,将一个变量标识为一条线程独占的状态
unlock 解锁 做用于主内存的变量,将一个标记为锁定状态的变量解锁,以便其它线程使用
read 读取 做用于主内存的变量,将一个变量的值从主内存传输到线程的工做内存中,以便load操做使用
load 载入 做用于工做内存的变量,将read操做读取过来的值放入工做内存的变量副本中
use 使用 做用于工做内存的变量,将工做内存的值传递给执行引擎,虚拟机遇到一个须要使用变量的字节码指令就会这么作
assign 赋值 做用于工做内存的变量,从执行引擎接受到的值赋给工做内存的变量,虚拟机遇到一个给变量赋值的字节码指令就会这么作
store 存储 做用于工做内存的变量,将工做内存的变量的值传递给主内存中,以便write操做
write 写入 做用于主内存的变量,它把store操做从工做内存中获得的变量赋值放入主内存的变量中

   Java内存模型只要求两个操做必须按顺序执行,而没有保证是连续执行,也就是说两个指令之间是能够有其它指令的。Java内存模型还规定可在执行上述8种基本操做时必须知足如下的规则:
   * 不容许read和load、store和write操做之一单独出现;
   * 不容许一个线程丢弃它最近的assign操做,即assign操做以后必须将值同步给主内存;
   * 不容许一个线程没发生过assign就把数据同步给主内存;
   * 一个新的变量只能诞生在主内存,不容许工做内存直接使用一个未被(load和assign)的变量;
   * 一个变量在同一时刻只容许同一条线程对其进行lock操做;
   * 若是对一个变量执行lock,那么将清空这个变量在工做内存的此变量的值,在执行引擎使用这个变量以前,从新执行load和assign操做初始化工做内存的值;
   * 若是一个变量事先没有被lock操做锁定,就容许对其或其它线程进行unlock操做;
   * 对一个变量执行unlock操做以前,必须先同步回主内存;

2.4.原子性(Atomicity)、可见性(Visibility)、有序性(Ordering)

   原子性(Atomicity):原子性是指一个操做是不可中断的,一旦一个操做开始,就不会被其它线程干扰。Java内存模型来直接保证原子性变量的操做包括read、load、assign、use、store和write,基本能够认为基本数据类型的读写是原子性的,可是double、long类型例外,这是它们的非原子性协定决定的。固然,Java内存模型还提供了lock和unlock来知足更大范围的原子性操做,这两个操做反映到字节码指令就是monitorenter和monitorexit隐式的操做,反映到代码上就是synchronized关键字。
   可见性(Visibility):可见性是指一个线程修改了共享变量的值,其它线程能当即得知这个更改。Java内存模型是经过变量修改后将新值同步给主内存,在变量读取前从主内存刷新变量值依赖主内存做为传递媒介的方式来实现可见性的,不管这个变量是否被volatile修饰,但它们的区别是volatile变量的特殊规则能当即同步到主内存,以及每次使用前从主内存刷新,而普通变量不行。固然,除了volatile能实现可见性以外,synchronized和final一样能够。synchronized的可见性是经过“对一个变量执行unlock操做以前,必须把此变量同步回主内存中”这条规则得到的;而final的可见性是指,被final修饰的字段在构造器一旦初始化完成,而且构造器没把this的引用传递出去,那么在其它线程就能看见final字段的值。
   有序性(Ordering):前面也提到,java会指令重排,代码顺序未必和指令执行顺序一致。Java提供了volatile和synchronized来保证线程之间操做的有序性。volatile关键字自己就禁止指令重排,而synchronized是由“一个变量在同一时刻只容许一条线程对其lock操做”这条规则得到。

2.5.Happen-Before原则

   Java里的有序性除了靠volatile和synchronized两个关键字完成,其实还隐藏着先行发生(Happen-Before)原则,经过这个原则和以前的规则基本能解决并发环境下两个操做之间的冲突问题。
   * 程序次序原则(Program Order Rule):一个线程内保证语义的串行;
   * 管程锁定原则(Monitor Lock Rule):unlock操做一定在以后的同一个锁的lock操做以前;
   * volatile规则(Volatile Variable Rule):volatile变量的写操做先行发生于后面这个变量的读操做;
   * 线程启动规则(Thread Start Rule):线程的start()方法先于该线程其它的每个动做;
   * 线程终止规则(Thread Termination Rule):线程的全部操做都先于该线程的终结(Thread.join());
   * 线程中断规则(Thread Interruption Rule):线程的interrupt()方法调用先行发生于被中断线程的代码检测到中断事件的发生;
   * 对象终结规则(Finalizer Rule):一个对象的初始化完成先行与它的finalize()方法的开始;
   * 传递性 (Transitivity):若是A操做先于B操做,B操做先于C操做,那么A一定先于C;

3.volatile

3.1.语义

   Java内存模型基本是围绕原子性、有序性、可见性展开,而volatile关键字的语义,一是保证此变量对全部线程的可见性,二是禁止指令重排。能够看出,volatile不能保证原子性,这个须要经过加锁或者一些原子类来实现。
   举个例子:

public class VolatileTest {

    public static volatile int i = 0;

    public static void increase() {
        i++;
    }

    public static class IncreaseTask implements Runnable{
        public void run() {
            for (int y = 0; y < 10000; y++) {
                increase();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new IncreaseTask());
            threads[i].start();
        }
        for (int i = 0; i < 10; i++) {
            threads[i].join();
        }
        System.out.println(i);
    }

}

   在上面这段代码中,变量i用volatile修饰,循环10个线程,每一个线程内部对i递增10000次,若是这段代码并发成功的话,预期的结果应该是100000。可是运行结果可见,每次的结果值都小于100000。
   这个正是由于increase()方法内部对i递增的处理,也就是 i++ 这一段代码不是原子的,代码虽然只有一行,可是编译出来的字节码指令却有多个指令,并且每一个指令自己未必就是原子的,由于这些指令还会转化成若干个本地机器码指令。不难分析出,每一个线程取到i的值那一刻,volatile保证了这一刻取到的是正确的数据,可是继续往下执行的时候,这个值就可能已经被其它线程修改了,而此时的数据就变成过时的数据,同步到主内存中的数据就多是一个较小的数据。
   除了在操做递增时候加锁以外,使用AtomicInteger原子类代替int同样能够获得预期的结果。

3.2.volatile的可见性和指令重排

   volatile修饰的变量,赋值后的指令会多出一个内存屏障,这个内存屏障会杜绝后面的指令排到前面去。这种内存屏障其实就是一个空操做,这个空操做指令是lock前缀,它的做用就是使得本CPU的Cache写入内存(write和store操做),该写入动做使得其它CPU或者别的内核无效化其Cache,因此经过这样的一个空操做,让volatile修饰的变量对其它CPU当即可见。也所以,这个空操做指令在同步到内存时,意味着全部的操做都已经执行完成,这样就造成了“指令排序没法越过屏障”的效果。

相关文章
相关标签/搜索