背景渊源 摩尔定律
提到多线程好多书上都会提到摩尔定律,它是由英特尔创始人之一Gordon Moore提出来的。其内容为:当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。这一定律揭示了信息技术进步的速度。
可是从2003年开始CPU主频已经不再翻倍,而是采用多核,而不是更快的主频。摩尔定律失效。那主频不再提高,核数增加的情况下要想让程序更快就要用到并行或并发编程。
并行与并发如果CPU主频增加程序不用做任何改动就能变快。但核多的话程序不做改动不一定会变快。
CPU厂商生产更多的核的CPU是可以的,一百多核也是没有问题的,但是软件还没有准备好,不能更好的利用,所以没有生产太多核的CPU。随着多核时代的来临,软件开发越来越关注并行编程的领域。但要写一个真正并行的程序并不容易。
并行和并发的目标都是最大化CPU的使用率,并发可以认为是一种程序的逻辑结构的设计模式。可以用并发的设计方式去设计模型,然后运行在一个单核的系统上。可以将这种模型不加修改的运行在多核系统上,实现真正的并行,并行是程序执行的一种属性真正的同时执行,其重点的是充分利用CPU的多个核心。
多线程开发的时候会有一些问题,比如安全性问题,一致性问题等,重排序问题,因为这些问题然后大家在写代码的时候会加锁等等。这些基础概念大家都懂,本文不再描述。本文主要分享造成这些问题的原因和JAVA解决这些问题的底层逻辑。
多线程 计算机存储体系要想明白数据一致性问题,要先缕下计算机存储结构,从本地磁盘到主存到CPU缓存,也就是从硬盘到内存,到CPU。一般对应的程序的操作就是从数据库查数据到内存然后到CPU进行计算。这个描述有点粗,下边画个图。
业内画这个图一般都是画的金字塔型状,为了证明是我自己画的我画个长方型的(其实我不会画金字塔)。
CPU多个核心和内存之间为了保证内部数据一致性还有一个缓存一致性协议(MESI),MESI其实就是指令状态中的首字母。M(Modified)修改,E(Exclusive)独享、互斥,S(Shared)共享,I(Invalid)无效。然后再看下边这个图。
太细的状态流转就不作描述了,扯这么多主要是为了说明白为什么会有数据一致性问题,就是因为有这么多级的缓存,CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题。解决一致性问题怎么办呢,两个思路。
锁住总线,操作时锁住总线,这样效率非常低,所以考虑第二个思路。
缓存一致性,每操作一次通知(一致性协议MESI),(但多线程的时候还是会有问题,后文讲)
JAVA内存模型上边稍微扯了一下存储体系是为了在这里写一下JAVA内存模型。
Java虚拟机规范中试图定义一种Java内存模型(java Memory Model) 来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
内存模型是内存和线程之间的交互、规则。与编译器有关,有并发有关,与处理器有关。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程中所说的变量有所区别,它包括 了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。为了获得较好的执行效能,Java内存模型并没有限制执行引擎使用处理器特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。
Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等 )都必需在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
这里所说的主内存、工作内存和Java内存区域中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的。 如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存对应Java堆中的对象实例数据部分 ,而工作内存则对应于虚拟机栈中的部分区域。从更底层次上说,主内存就是直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。