计算机的定义:java
接受使用者输入指令与数据, 经由中央处理器的数学与逻辑单元运算处理后,以产生或储存成有用的信息
咱们的我的电脑也是计算机的一种,,依外观来看这家伙主要分三部分:程序员
而咱们今天研究的主题就是计算机其中的主机部分。整部主机的重点在于中央处理器(cpu),cpu是一个具备特定功能的芯片,
里面含有不少微指令集,计算机全部的功能都须要微指令集的支持才能够完成。cpu的主要做用在于管理和运算,所以cpu内部又可分为两个单元,分别为:算数逻辑单元和控制单元。其中算数逻辑单元主要负责程序运算和逻辑判断,控制单元主要负责和各周边主件与各单元之间的工做。缓存
上图所展现的系统单元其实就是主机的主要组件,其中的核心就是cpu和主内存。基本上全部数据都要通过主内存,至因而流入仍是流出则是cpu所发布的控制指令,而cpu实际要处理的数据则所有来自于主内存!安全
cpu做为计算机的大脑,由于许多运算和逻辑都在cpu里处理,因此须要其拥有很强大的处理能力,但外部组件的速度和cpu的速度相差实在太多,才啊有了所谓的外频和倍频。
所谓外频指的是cpu与外部组件进行数据传输的速度。倍频则是cpu内部用来加速工做的一个倍数。二者相乘才是cpu本身的主频。多线程
程序的启动和运转有着一个重要的问题,即系统花费了大量的时间把信息从一个地方挪到另外一个地方。数据最初放在磁盘上,当程序被加载时,将其移动到主内存,当程序运行时,指令又从内存复制到cpu上。从程序员的角度来看,这些复制就是开销,是减慢了程序运行速度的罪魁祸首。所以,系统设计者设计了高速缓存来使这些复制操做尽量快地完成。
一个系统上磁盘驱动器可能比主内存大100倍,可是对处理器来讲,从磁盘驱动器读取一个字的开销比从主内存读取的开销大1000万倍。相似的,一个寄存器只能够储存几百字节的信息,而主内存里能够放几十亿字节。然而寄存器的速度大约是主内存的100倍。并且,随着半导体技术的进步,这种处理器与主存之间的差距还在持续增大。
针对这种处理器与主存之间的差别,系统设计者采用了更小更快的存储设备,称为高速缓存存储器。其中又分为L一、L二、L3高速缓存,限于篇幅,在这里就不给你们详细介绍了.系统经过让高速缓存里存放可能常常访问的数据,大部分的内存操做都能在快速的高速缓存中完成。架构
多任务处理器在现代计算机系统中几乎已经是一项必备的功能了。全部的运算任务至少都要与主内存交互才能完成,因为计算机的存储设备和处理器的运算速度之间存在着几个数量级的差距。因此现代计算机系统都不得不加入一层读写速度尽量接近于处理器的高速缓存来做为内存与处理器之间的缓冲:将运算须要使用的数据复制到缓存中,让运算高速进行,当运算结束后,再将缓存中的结果复制到主内存中。这样处理器就不须要等待缓慢的内存读写了。以下图所示:并发
看似很美好,实际上并无想象中的那么容易。在计算机系统中,可能存在多个处理器,每一个处理器都有本身的高速缓存,而他们又共享同一主内存。当多个处理器的运算任务涉及到统一块内存区域,将可能致使高速缓存间的不一致,那同步到主内存以哪一个为准呢?为了解决一致性问题,须要各个处理器访问缓存须要遵循一些一致性协议来进行操做。java内存模型定义的内存访问操做和硬件的访问操做是有可比性的。
java虚拟机规范试图定义一种java内存模型来屏蔽掉各类硬件和操做系统的访问差别,以实现让java程序在任何机器上都能达到一致的相同效果。所以定义java内存模型是一件很是麻烦的事,既要足够严谨,让java的并发操做不会发生歧义;但也必须足够宽松,使虚拟机的实现有足够的自由空间去利用硬件的各类特性(寄存器、高速缓存等)来获取更好的执行速度。内存模型以下图所示:app
在讲重排序以前,咱们先来看一段代码:jvm
public class ReOrderTest { private static int x = 0, y = 0; private static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { int i = 0; for (; ; ) { i++; x = 0;y = 0;a = 0;b = 0; CountDownLatch latch = new CountDownLatch(1); Thread one = new Thread(() -> { try { latch.await(); } catch (InterruptedException e) { } a = 1; x = b; }); Thread other = new Thread(() -> { try { latch.await(); } catch (InterruptedException e) { } b = 1; y = a; }); one.start();other.start(); latch.countDown(); one.join();other.join(); String result = "第" + i + "次 (" + x + "," + y + ")"; if (x == 0 && y == 0) { System.err.println(result); break; } else { System.out.println(result); } } } }
看完这段代码,或许没有接触太重排序的同窗会认为这是一个死循环,其输出结果只会有(1,1),(1,0),(0,1)三种结果。但实际上只须要运行几秒钟,就会break出来,出现x=0;y=0的状况。
重排序由如下几种机制引发的:工具
刚才再讲重排序的时候,就提到了内存可见性。线程1执行a=1,这个结果对于线程2来讲不必定可见。这种不可见不是因为多处理器形成的,而是因为多缓存形成的。如今每一个处理器上都会有寄存器,L一、L二、L3缓存等等,问题就发生在每一个处理器都独占一个缓存,数据修改刷入缓存,而后从缓存刷入内存,因此就会致使有些处理器读到的是过时的值。java做为高级语言,为咱们抽象jmm模型,定义了读写数据的规范,使咱们不用关心缓存的概念,可是jmm也同时给咱们抽象出了工做内存和主内存。(ps:这里说的工做内存是对寄存器,L一、L二、L3缓存等的一个抽象)
happens-before是理解jmm最核心的概念。对于java程序员来讲,若是你想理解并写好并发程序,happens-before是理解jmm模型的关键。
《JSR-133:Java Memory Model and Thread Specification》对happens-before关系的定义以下:
1)若是一个操做happens-before另外一个操做,那么第一个操做的执行结果将对第二个操做可见,并且第一个操做的执行顺序排在第二个操做以前。
2)两个操做之间存在happens-before关系,并不意味着Java平台的具体实现必需要按照happens-before关系指定的顺序来执行。若是重排序以后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM容许这种重排序)。
public static int getz() { int x=1; //A int y=1; //B int z=x+y; //C return z; }
上面的代码示例存在了3个happens-before规范:
其中二、3是必须的,而1不是必需的。所以jmm又把happens-before要求禁止的重排序分为了如下两种:
happens-before规则
《JSR-133:Java Memory Model and Thread Specification》定义了以下happens-before规则。
1)程序顺序规则:一个线程中的每一个操做,happens-before于该线程中的任意后续操做。
2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的
读。
4)传递性:若是A happens-before B,且B happens-before C,那么A happens-before C。
5)start()规则:若是线程A执行操做ThreadB.start()(启动线程B),那么A线程的
ThreadB.start()操做happens-before于线程B中的任意操做。
6)join()规则:若是线程A执行操做ThreadB.join()并成功返回,那么线程B中的任意操做
happens-before于线程A从ThreadB.join()操做成功返回。
咱们其中最多见的就是一、二、三、4.其中一、4的状况在前面已经讨论过。3)将会在volatile的内存语义中进行讨论。如今咱们来看下锁的释放-获取创建的happens-before关系:
int a=0; public synchronized void read(){//1 a++;//2 }//3 public synchronized void writer(){//4 int i=a+1;//5 }//6
由程序顺序规则来判断:1happens-before2,2happens-before3,4happens-before5,5happens-before6.
由监视器锁规则来判断:3happens-before4
由传递性来判断:1happens-before2,2happens-before3,3happens-before4,4happens-before5,5happens-before6
怎么实现的呢?进入锁的时候将会使工做内存失效,读取变量必须从主内存中读取。释放锁得时候会将该变量刷新回主内存。这里的锁包括conuurent包下的锁.
关于volatile,你们只须要牢记两点:内存可见和禁止重排序.
关于volatile的可见性,常常被你们误解。认为volatile变量对全部线程是当即可见的,对volatile变量全部的写操做都能马上反映到其余县城中,换句话说,volatile变量的运算在并发下是安全的。这个结论是错误的,虽然volatile变量能够保证可见性,可是java里面的运算并不是原子操做,致使volatile变量的运算在并发下同样是不安全的。请看代码示例:
public class BubbleSort { static volatile int a; public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[20]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new Runnable() { public void run() { for (int x = 0; x < 10000; x++) { add(); } } }); threads[i].start(); } while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println("a=" + a); } private static void add() { a++; } } 输出结果:a=159957
结果具备不肯定性,缘由就是a++自增运算,不是一个原子性操做。经过javap -c BubbleSort.class反编译这段代码获得add()的字节码文件,以下图所示:
能够看到a++这个运算操做产生了4条字节码(return 不是a++产生的),volatile只能保证getstatic时得到到a的值是正确的,当执行其余指令时,颇有可能a已是过时数据了。事实上这样分析是不太严谨的,由于字节码最终会变成cpu指令执行,即便只编译出一条字节码指令也不能保证这个指令就是原子操做。因此若是当咱们进行运算的时候,仍要经过加锁或者使用concurrent并发包下的原子类才能保证其原子性。
禁止重排序有一个很是经典的例子,就是DCL单例模式.关于这篇文章,大神们早已发过文章对此进行阐述了,这里搬运一下:
来膜拜下文章署名中的大神们:David Bacon (IBM Research) Joshua Bloch (Javasoft), Jeff Bogda, Cliff Click (Hotspot JVM project), Paul Haahr, Doug Lea, Tom May, Jan-Willem Maessen, Jeremy Manson, John D. Mitchell (jGuru) Kelvin Nilsen, Bill Pugh, Emin Gun Sirer,至少 Joshua Bloch 和 Doug Lea 你们都不陌生吧。
话很少说,上例子:
public class Singleton { private static Singleton instance = null; private Singleton() { this.v = 3; } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
jvm接收到new指令时,简单分为3步(实际更多,可参考深刻理解虚拟机),1分配内存2实例化对象3将内存地址指向引用。java的内存模型并不限制指令的重排序,也就说当执行步骤从1-》2-》3变成1-》3-》2。当线程a访问走到第2步,未完成实例化对象前,线程b访问此对象的返回一个引用,但如果进行其余操做,由于对象并无实例化,会形成this逃逸的问题。解决的方法很简单,就是加上volatile关键字。
volatile小结
描述该类知识须要很是严谨的描述,虽然我仔细检查了好几遍,但仍担忧会出错,一来受限于有限的知识储备,二来受限于蹩脚的文字表达能力。但愿读者能够帮助我指正表达错误的地方.