本篇文章基本上都是概念性的知识,理解记忆为主:
1、Java内存结构
java文件,首先要经过编程成为class文件,然后通过类装载器加载到jvm中去执行。这个jvm(蓝色线框起来的这部分)就是java运行时数据区,意思就是java代码在运行的时候,这些数据要存放在不同的内存空间里面。jvm就是指代这个的。当然了上面的运行时数据区jvm是jdk1.7版本的。也就是说不同的jdk版本,这个jvm的构成是不一样的。如下是Java7的内存结构:
我们可以看到一共划分了5个部分,其中java堆区和方法区还是所有线程共享的区域。那么为什么要设置成线程共享的呢?因为假设一个数据,每个线程都保留一份,那其中有一个线程将这个数据更改了。其他的线程发现自己的数据没有变,这就出现了问题了。于是设计成了所有线程共享,java内存模型出来了。
2、JMM
Java的内存模型也叫做JMM。但这个模型不是像内存结构一样,是真实存在的。java内存模型是一个抽象出来的概念。意思是把一部分内存区域设计成所有线程共享的,一个线程对数据更改,其他线程就能立刻知道。
Java内存结构和Java内存模型的区别
(1)java内存结构是解决java中的数据如何存放的问题。
(2)java内存模型是解决java中多个线程共享数据的问题。
Java内存模型的来源
阶段一
在计算机发展的第一个阶段,程序是在CPU中运行,数据在主存中保存。随着技术的发展,CPU的速度越来越多高,但是主存的速度却没有提高太多。:
阶段二
为了解决上面的问题,于是乎出现了缓存,里面存放了一些CPU经常使用的主存数据,缓存的速度和CPU差不多,当CPU查找数据的时候,首先从缓存中查找,没有的话再从主存中查找。写数据的时候,先写缓存的数据,然后再更新到主存中。这样一种机制使得速度提高很多。
阶段三
技术继续发展,在上面缓存的基础上出现了一级二级三级缓存,查找也是逐层的,第一级缓存没有就到第二层,就这样以此类推。这时候CPU也得到了快读发展,由之前的一个核变成了多核CPU(一个CPU变成了多部分)。
这时候呢,之前只能同时跑一个线程,现在就能跑很多个线程了。而且从上面我们可以看到,每一个核都有相应的缓存区,但是主内存还是哪一个。但是这样操作却出现了很多问题。
问题一:缓存一致性问题
比如主存中的a=0,此时内核1和内核2都想同时对这个a进行操作,内核1在内核2改变a的值之前就将a=0的值读取,但是随后内核2又将a赋值为1,此时内核1就读到了脏数据,这就是一个很严重的问题。
问题二:处理器优化和指令重排
这问题的意思是,既然CPU有这么多内核,肯定是想让资源得到充分利用,于是把我们写的程序拆分,对一些代码进行乱序处理,这就是处理器优化。而且,java虚拟机就模仿了一下,创建了即时编译器(JIT),这个编译器也会做指令重排的操作。很明显,我们的代码顺序被打乱,指令被重排,就可能不会按照我们的意愿去执行了。
上面出现的这些问题,都是从硬件的角度来分析的,**《深入理解java虚拟机》**一书于是引出了软件的问题,那么上面的这些问题如果转化到软件层次会带来什么问题呢?
问题三:软件问题
(1)原子性问题
首先缓存一致性问题在程序中会带来原子性问题,原子性问题是什么意思呢?你首先就要先理解原子。在生物里面原子叫做不可再分的物质。在软件里面,原子叫做不可再分的程序操作。而原子性问题肯定就是打破了这个规则,也就是说在这个操作中又进行了拆分。
在缓存一致性问题中,两个CPU内核中a的数据不一致,也就是说两个CPU内核读取主存a的值是不一样的。那么对于a的更改这个操作肯定就不是原子性,在A更改的过程中,两个CPU内核同时进行了更改。(CAS)
(2)可见性问题
上面在介绍原子性问题的时候说了,两个线程(CPU内核)访问同一个变量时,线程2修改了这个变量的值,但是线程1却没有看到其修改,所以读的仍然是旧数据。(volatile,sync…)
(3)有序性
也就是程序没有按照指定的顺序去执行。可见性问题和有序性问题就是由于处理器优化和执行重排造成的。(volatile,内存屏障)
那么Java是如何解决上面的问题的呢?
规则一:所有的数据都在主内存中。
规则二:每个线程都保留一份共享变量的副本。线程对变量的所有操作都必须在这个副本内存中进行,而不能直接读写主内存。
规则三:不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
那就是java提供了什么东西来实现的这三个规则呢?
这里举个例子让大家理解一下:
你(线程)到一个饭馆(主内存)去吃饭(数据),你只能吃饭馆给你上到桌子上的菜,而不能直接到饭馆的后厨去拿菜吃【规则2】;这个时候遇到朋友要和你一起吃,你要给你的朋友重新点菜上菜而不是让你的朋友吃你剩下的汤汤水水【规则3】;
同时Java中也用了很多方法来保证解决上面的三个问题,比如说synchronized关键字保证了原子性,volatile关键字保证了可见性。synchronized关键字和volatile关键字保证了有序性。当然还有很多的Lock机制,并发包里面等等都是为了解决这三个问题提出来的。(此处不再由详细解释,以后会专门出文章讲解)
happen-before原则:
在JVM内存模型中,如果一个操作要对另一个操作可见,就必须实现happen-before原则(不要求执行顺序,只要求一个结果对另一个结果可见)
happen-before原则内容:
1、程序次序原则:一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。
2、监视器锁规则:一个unlock操作先行发生于后面对同一个锁对象的lock操作。也就是如果同一个锁是锁定状态,那么必须先对其unlock之后才能lock;
3、volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,指时间上的先后顺序;
4、线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作;
5、线程终止规则:线程中所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join方法结束,thread.isAlive的返回值等手段检测线程已经被终止执行;
6、线程中断原则:对线程Interrupt方法的调用先行发生于被中断线程的代码监测到中断时间的发生,可以通过interrupt方法检测是否发生中断;
7、对象终结规则:一个对象的初始化完成,先行发生于它的finalize方法的开始;
8、传递性:如果A先行发生于B,B先行发生于C,则A先行发生于C;