提到多线程好多书上都会提到摩尔定律,它是由英特尔创始人之一Gordon Moore提出来的。其内容为:当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增长一倍,性能也将提高一倍。换言之,每一美圆所能买到的电脑性能,将每隔18-24个月翻一倍以上。这必定律揭示了信息技术进步的速度。java
但是从2003年开始CPU主频已经再也不翻倍,而是采用多核,而不是更快的主频。摩尔定律失效。那主频再也不提升,核数增长的状况下要想让程序更快就要用到并行或并发编程。数据库
若是CPU主频增长程序不用作任何改动就能变快。但核多的话程序不作改动不必定会变快。编程
CPU厂商生产更多的核的CPU是能够的,一百多核也是没有问题的,可是软件尚未准备好,不能更好的利用,因此没有生产太多核的CPU。随着多核时代的来临,软件开发愈来愈关注并行编程的领域。但要写一个真正并行的程序并不容易。设计模式
并行和并发的目标都是最大化CPU的使用率,并发能够认为是一种程序的逻辑结构的设计模式。能够用并发的设计方式去设计模型,而后运行在一个单核的系统上。能够将这种模型不加修改的运行在多核系统上,实现真正的并行,并行是程序执行的一种属性真正的同时执行,其重点的是充分利用CPU的多个核心。数组
多线程开发的时候会有一些问题,好比安全性问题,一致性问题等,重排序问题,由于这些问题而后你们在写代码的时候会加锁等等。这些基础概念你们都懂,本文再也不描述。本文主要分享形成这些问题的缘由和JAVA解决这些问题的底层逻辑。缓存
要想明白数据一致性问题,要先缕下计算机存储结构,从本地磁盘到主存到CPU缓存,也就是从硬盘到内存,到CPU。通常对应的程序的操做就是从数据库查数据到内存而后到CPU进行计算。这个描述有点粗,下边画个图。安全
业内画这个图通常都是画的金字塔型状,为了证实是我本身画的我画个长方型的(其实我不会画金字塔)。多线程
CPU多个核心和内存之间为了保证内部数据一致性还有一个缓存一致性协议(MESI),MESI其实就是指令状态中的首字母。M(Modified)修改,E(Exclusive)独享、互斥,S(Shared)共享,I(Invalid)无效。而后再看下边这个图。并发
太细的状态流转就不做描述了,扯这么多主要是为了说明白为何会有数据一致性问题,就是由于有这么多级的缓存,CPU的运行并非直接操做内存而是先把内存里边的数据读到缓存,而内存的读和写操做的时候就会形成不一致的问题。解决一致性问题怎么办呢,两个思路。app
上边稍微扯了一下存储体系是为了在这里写一下JAVA内存模型。
Java虚拟机规范中试图定义一种Java内存模型(java Memory Model) 来屏蔽掉各类硬件和操做系统的内存访问差别,以实现让Java程序在各类平台下都能达到一致的内存访问效果。
内存模型是内存和线程之间的交互、规则。与编译器有关,有并发有关,与处理器有关。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程中所说的变量有所区别,它包括 了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,由于后者是线程私有的,不会被共享,天然就不会存在竞争问题。为了得到较好的执行效能,Java内存模型并无限制执行引擎使用处理器特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。
Java内存模型规定了全部的变量都存储在主内存中。每条线程还有本身的工做内存,线程的工做内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的全部操做(读取,赋值等 )都必需在工做内存中进行,而不能直接读写主内存中的变量。不一样的线程之间也没法直接访问对方工做内存中的变量,线程间变量值的传递均须要经过主内存来完成。
这里所说的主内存、工做内存和Java内存区域中的Java堆、栈、方法区等并非同一个层次的内存划分,这二者基本上是没有关系的。 若是二者必定要勉强对应起来,那从变量、主内存、工做内存的定义来看,主内存对应Java堆中的对象实例数据部分 ,而工做内存则对应于虚拟机栈中的部分区域。从更底层次上说,主内存就是直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工做内存优先存储于寄存器和高速缓存中,由于程序运行时主要访问读写的是工做内存。
前边说的都是和内存有关的内容,其实多线程有关系的还有指令重排序,指令重排序也会形成在多线程访问下结束和想的不同的状况。大段的介绍就不写了要不篇幅太长了(JVM那里书里边有)。主要就是在CPU执行指令的时候会进行执行顺序的优化。画个图看一下吧。
具体理论后文再写先来点干货,直接上代码,一看就明白。
public class HappendBeforeTest { int a = 0; int b = 0; public static void main(String[] args) { HappendBeforeTest test = new HappendBeforeTest(); Thread threada = new Thread() { @Override public void run() { test.a = 1; System.out.println("b=" + test.b); } }; Thread threadb = new Thread() { @Override public void run() { test.b = 1; System.out.println("a=" + test.a); } }; threada.start(); threadb.start(); } }
猜猜有可能输出什么?多选
A:a=0,b=1 B:a=1,b=0 C:a=0,b=0 D:a=1,b=1
上边这段代码不太好调,而后我稍微改造了一下。
public class HappendBeforeTest { static int a = 0; static int b = 0; static int x = 0; static int y = 0; public static void shortWait(long interval) { long start = System.nanoTime(); long end; do { end = System.nanoTime(); } while (start + interval >= end); } public static void main(String[] args) throws InterruptedException { for (; ; ) { Thread threada = new Thread() { @Override public void run() { a = 1; x = b; } }; Thread threadb = new Thread() { @Override public void run() { b = 1; y = a; } }; Thread starta = new Thread() { @Override public void run() { // 因为线程threada先启动 //下面这句话让它等一等线程startb shortWait(100); threada.start(); } }; Thread startb = new Thread() { @Override public void run() { threadb.start(); } }; starta.start(); startb.start(); starta.join(); startb.join(); threada.join(); threadb.join(); a = 0; b = 0; System.out.print("x=" + x); System.out.print("y=" + y); if (x == 0 && y == 0) { break; } x = 0; y = 0; System.out.println(); } } }
这段代码,a和b初始值为0,而后两个线程同时启动分别设置a=1,x=b和b=1,y=a。这个代码里边的starta和startb线程彻底是为了让threada 和threadb 两个线程尽可能同时启动而加的,里边只是分别调用了threada 和threadb 两个线程。而后无限循环只要x和y 不一样时等于0就初始化全部值继续循环,直到x和y都是0的时候break。你猜猜会不会break。
结果看截图
由于我没有记录循环次数,不知道循环了几回,而后触发了条件break了。从代码上看,在输出A以前必然会把B设置成1,在输出B以前必然会把A设置为1。那为何会出现同时是零的状况呢。这就颇有多是指令被重排序了。
指令重排序简单了说是就两行以上不相干的代码在执行的时候有可能先执行的不是第一条。也就是执行顺序会被优化。
如何判断你写的代码执行顺序会不会被优化,要看代码之间有没有Happens-before
关系。Happens-before
就是不无需任何干涉就能够保证有有序执行,因为篇幅限制Happens-before
就不在这里多作介绍。
下面简单介绍一下java里边的一个关键字volatile
。volatile
简单来讲就是来解决重排序问题的。对一个volatile
变量的写,必定happen-before
后续对它的读。也就是你在写代码的时候不但愿你的代码被重排序就使用volatile
关键字。volatile
还解决了内存可见性问题,在执行执行的时候一共有8条指令lock(锁定)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)、unlock(解锁)(篇幅限制具体指令内容自行查询,看下图大概有个了解)。
volatile
主要是对其中4条指令作了处理。以下图
也就是把 load和use关联执行,把assign和store关联执行。众所周知有load必需有read如今load又和use关联也就是要在缓存中要use的时候就必需要load要load就必须要read。通俗讲就是要use(使用)一个变量的时候必需load(载入),要载入的时候必需从主内存read(读取)这样就解决了读的可见性。下面看写操做它是把assign和store作了关联,也就是在assign(赋值)后必需store(存储)。store(存储)后write(写入)。也就是作到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存。就这样经过用的时候直接从主内存取,在赋值到直接写回主内存作到了内存可见性。
我在网上看到大部分写多线程的时候都会写到锁,AQS和线程池。因为网文太多本文就很少作介绍。下面简单写一写CAS。
CAS是一个比较魔性的操做,用的好可让你的代码更优雅更高效。它就是无锁编程的核心。
CAS书上是这么介绍的:“CAS即Compare and Swap,是JDK提供的非阻塞原子性操做,它经过硬件保证了比较-更新的原子性”。他是非阻塞的仍是原子性,也就是说这玩意效率更高。仍是经过硬件保证的说明这玩意更可靠。
从上图能够看出,在cas指令修改变量值的时候,先要进行值的判断,若是值和原来的值相等说明尚未被其它线程改过,则执行修改,若是被改过了,则不修改。在java里边java.util.concurrent.atomic
包下边的类都使用了CAS操做。最经常使用的方法就是compareAndSet
。其底层是调用的Unsafe
类的compareAndSwap
方法。
做者:高玉珑