本文是两章的笔记整理。html
CPU
缓存计算机中的全部运算操做都是由CPU
完成的,CPU
指令执行过程须要涉及数据读取和写入操做,可是CPU
只能访问处于内存中的数据,而内存的速度和CPU
的速度是远远不对等的,所以就出现了缓存模型,也就是在CPU
和内存之间加入了缓存层。通常现代的CPU
缓存层分为三级,分别叫L1
缓存、L2
缓存和L3
缓存,简略图以下:java
L1
缓存:三级缓存中访问速度最快,可是容量最小,另外L1
缓存还被划分红了数据缓存(L1d
,data
首字母)和指令缓存(L1i
,instruction
首字母)L2
缓存:速度比L1
慢,可是容量比L1
大,在现代的多核CPU
中,L2
通常被单个核独占L3
缓存:三级缓存中速度最慢,可是容量最大,现代CPU
中也有L3
是多核共享的设计,好比zen3
架构的设计缓存的出现,是为了解决CPU
直接访问内存效率低下的问题,CPU
进行运算的时候,将须要的数据从主存复制一份到缓存中,由于缓存的访问速度快于内存,在计算的时候只须要读取缓存并将结果更新到缓存,运算结束再将结果刷新到主存,这样就大大提升了计算效率,总体交互图简略以下:编程
虽然缓存的出现,大大提升了吞吐能力,可是,也引入了一个新的问题,就是缓存不一致。好比,最简单的一个i++
操做,须要将内存数据复制一份到缓存中,CPU
读取缓存值并进行更新,先写入缓存,运算结束后再将缓存中新的刷新到内存,具体过程以下:缓存
i
到缓存中CPU
读取缓存i
中的值i
进行加1操做这样的i++
操做在单线程不会出现问题,但在多线程中,由于每一个线程都有本身的工做内存(也叫本地内存,是线程本身的缓存),变量i
在多个线程的本地内存中都存在一个副本,若是有两个线程执行i++
操做:多线程
i
初始值为0i
的值放入缓存中,此时i
的值为0,线程B也同理,放入缓存中的值也是0i
的值都是1i
写入主内存,至关于i
被两次赋值为1i
的值为1这个就是典型的缓存不一致问题,主流的解决办法有:架构
这是一种悲观的实现方式,具体来讲,就是经过处理器发出lock
指令,锁住总线,总线收到指令后,会阻塞其余处理器的请求,直到占用锁的处理器完成操做。特色是只有一个抢到总线锁的处理器运行,可是这种方式效率低下,一旦某个处理器获取到锁其余处理器只能阻塞等待,会影响多核处理器的性能。并发
图示以下:app
缓存一致性协议中最出名的就是MESI
协议,MESI
保证了每个缓存中使用的共享变量的副本都是一致的。大体思想是,CPU
操做缓存中的数据时,若是发现该变量是一个共享变量,操做以下:ide
CPU
将该变量的缓存行设置为无效状态(Invalid
),其余CPU
进行该变量的读取时须要到主存中再次获取具体来讲,MESI
中规定了缓存行使用4种状态标记:高并发
M
:Modified
,被修改E
:Exclusive
,独享的S
:Shared
,共享的I
:Invalid
,无效的有关MESI
详细的实现超出了本文的范围,想要详细了解能够参考此处或此处。
JMM
看完了CPU
缓存再来看一下JMM
,也就是Java
内存模型,指定了JVM
如何与计算机的主存进行工做,同时也决定了一个线程对共享变量的写入什么时候对其余线程可见,JMM
定义了线程和主内存之间的抽象关系,具体以下:
JMM
内存模型同样也是一个抽象概念,其实并不存在,涵盖了缓存、寄存器、编译期优化以及硬件等简略图以下:
与MESI
相似,若是一个线程修改了共享变量,刷新到主内存后,其余线程读取工做内存的时候发现缓存失效,会从主内存再次读取到工做内存中。
而下图表示了JVM
与计算机硬件分配的关系:
文章都看了大半了还没到volatile
?别急别急,先来看看并发编程中的三个重要特性,这对正确理解volatile
有很大的帮助。
原子性就是在一次或屡次操做中:
一个典型的例子就是两我的转帐,好比A向B转帐1000元,那么这包含两个基本的操做:
这两个操做,要么都成功,要么都失败,也就是不能出现A帐户扣除1000可是B帐户金额不变的状况,也不能出现A帐户金额不变B帐户增长1000的状况。
须要注意的是两个原子性操做结合在一块儿未必是原子性的,好比i++
。本质上来讲,i++
涉及到了三个操做:
get i
i+1
set i
这三个操做都是原子性的,可是组合在一块儿(i++
)就不是原子性的。
另外一个重要的特性是可见性,可见性是指,一个线程对共享变量进行了修改,那么另外的线程能够当即看到修改后的最新值。
一个简单的例子以下:
public class Main { private int x = 0; private static final int MAX = 100000; public static void main(String[] args) throws InterruptedException { Main m = new Main(); Thread thread0 = new Thread(()->{ while(m.x < MAX) { ++m.x; } }); Thread thread1 = new Thread(()->{ while(m.x < MAX){ } System.out.println("finish"); }); thread1.start(); TimeUnit.MILLISECONDS.sleep(1); thread0.start(); } }
线程thread1
会一直运行,由于thread1
把x
读入工做内存后,会一直判断工做内存中的值,因为thread0
改变的是thread0
工做内存的值,并无对thread1
可见,所以永远也不会输出finish
,使用jstack
也能够看到结果:
有序性是指代码在执行过程当中的前后顺序,因为JVM
的优化,致使了代码的编写顺序未必是代码的运行顺序,好比下面的四条语句:
int x = 10; int y = 0; x++; y = 20;
有可能y=20
在x++
前执行,这就是指令重排序。通常来讲,处理器为了提升程序的效率,可能会对输入的代码指令作必定的优化,不会严格按照编写顺序去执行代码,但能够保证最终运算结果是编码时的指望结果,固然,重排序也有必定的规则,须要严格遵照指令之间的数据依赖关系,并非能够任意重排序,好比:
int x = 10; int y = 0; x++; y = x+1;
y=x+1
就不能先优于x++
执行。
在单线程下重排序不会致使预期值的改变,但在多线程下,若是有序性得不到保证,那么将可能出现很大的问题:
private boolean initialized = false; private Context context; public Context load(){ if(!initialized){ context = loadContext(); initialized = true; } return context; }
若是发生了重排序,initialized=true
排序到了context=loadContext()
的前面,假设两个线程A、B同时访问,且loadContext()
须要必定耗时,那么:
true
,再进行loadContext()
操做true
,会直接返回一个未加载完成的context
volatile
好了终于到了volatile
了,前面说了这么多,目的就是为了能完全理解和明白volatile
。这部分分为四个小节:
volatile
的语义synchronized
区别先来介绍一下volatile
的语义。
被volatile
修饰的实例变量或者类变量具备两层语义:
先说结论:
volatile
能保证可见性volatile
能保证有序性volatile
不能保证原子性下面分别进行介绍。
Java
中保证可见性有以下方式:
volatile
:当一个变量被volatile
修饰时,对共享资源的读操做会直接在主内存中进行(准确来讲也会读取到工做内存中,可是若是其余线程进行了修改就必须从主内存从新读取),写操做是先修改工做内存,可是修改结束后当即刷新到主内存中synchronized
:synchronized
同样能保证可见性,可以保证同一时刻只有一个线程获取到锁,而后执行同步方法,而且确保锁释放以前,变量的修改被刷新到主内存中Lock
:Lock
的lock
方法能保证同一时刻只有一个线程可以获取到锁而后执行同步方法,而且确保锁释放以前可以将对变量的修改刷新到主内存中具体来讲,能够看一下以前的例子:
public class Main { private int x = 0; private static final int MAX = 100000; public static void main(String[] args) throws InterruptedException { Main m = new Main(); Thread thread0 = new Thread(()->{ while(m.x < MAX) { ++m.x; } }); Thread thread1 = new Thread(()->{ while(m.x < MAX){ } System.out.println("finish"); }); thread1.start(); TimeUnit.MILLISECONDS.sleep(1); thread0.start(); } }
上面说过这段代码会不断运行,一直没有输出,就是由于修改后的x
对线程thread1
不可见,若是在x
的定义中加上了volatile
,就不会出现没有输出的状况了,由于此时对x
的修改是线程thread1
可见的。
JMM
中容许编译期和处理器对指令进行重排序,在多线程的状况下有可能会出现问题,为此,Java
一样提供了三种机制去保证有序性:
volatile
synchronized
Lock
另外,关于有序性不得不提的就是Happens-before
原则。Happends-before
原则说的就是若是两个操做的执行次序没法从该原则推导出来,那么就没法保证有序性,JVM
或处理器能够任意重排序。这么作的目的是为了尽量提升程序的并行度,具体规则以下:
unlock
操做要先行发生于对同一个锁的lock
操做volatile
变量规则:对一个变量的写操做要早于对这个变量以后的读操做Thread
对象的start()
方法先行发生于对该线程的任何动做interrupt()
方法确定要优于捕获到中断信号,换句话说,若是收到了中断信号,那么在此以前一定调用了interrupt()
finalize()
以前对于volatile
,会直接禁止对指令重排,可是对于volatile
先后无依赖关系的指令能够随意重排,好比:
int x = 0; int y = 1; //private volatile int z; z = 20; x++; y--;
在z=20
以前,先定义x
或先定义y
并无要求,只须要在执行z=20
的时候,能够保证x=0,y=1
便可,同理,x++
或y--
具体先执行哪个并无要求,只须要保证二者执行在z=20
以后便可。
在Java
中,全部对基本数据类型变量的读取赋值操做都是原子性的,对引用类型的变量读取和赋值也是原子性的,可是:
i++
JMM
只保证基本读取和赋值的原子性操做,其余的均不保证,若是须要具有原子性,那么可使用synchronized
或Lock
,或者JUC
包下的原子操做类也就是说,volatile
并不能保证原子性,例子以下:
public class Main { private volatile int x = 0; private static final CountDownLatch latch = new CountDownLatch(10); public void inc() { ++x; } public static void main(String[] args) throws InterruptedException { Main m = new Main(); IntStream.range(0, 10).forEach(i -> { new Thread(() -> { for (int j = 0; j < 1000; j++) { m.inc(); } latch.countDown(); }).start(); }); latch.await(); System.out.println(m.x); } }
最后输出的x
的值会少于10000
,并且每次运行的结果也并不相同,至于缘由,能够从两个线程A、B开始分析,图示以下:
0-t1
:线程A将x
读入工做内存,此时x=0
t1-t2
:线程A时间片完,CPU
调度线程B,线程B将x
读入工做内存,此时x=0
t2-t3
:线程B对工做内存中的x
进行自增操做,并更新到工做内存中t3-t4
:线程B时间片完,CPU
调度线程A,同理线程A对工做内存中的x
自增t4-t5
:线程A将工做内存中的值写回主内存,此时主内存中的值为x=1
t5
之后:线程A时间片完,CPU
调度线程B,线程B也将本身的工做内存写回主内存,再次将主内存中的x
赋值为1也就是说,多线程操做的话,会出现两次自增可是实际上只进行一次数值修改的操做。想要x
的值变为10000
也很简单,加上synchronized
便可:
new Thread(() -> { synchronized (m) { for (int j = 0; j < 1000; j++) { m.inc(); } } latch.countDown(); }).start();
前面已经知道,volatile
能够保证有序性以及可见性,那么,具体是如何操做的呢?
答案就是一个lock;
前缀,该前缀实际上至关于一个内存屏障,该内存屏障会为指令的执行提供以下几个保障:
一个典型的使用场景是利用开关进行线程的关闭操做,例子以下:
public class ThreadTest extends Thread{ private volatile boolean started = true; @Override public void run() { while (started){ } } public void shutdown(){ this.started = false; } }
若是布尔变量没有被volatile
修饰,那么极可能新的布尔值刷新不到主内存中,致使线程不会结束。
synchronized
的区别volatile
只能用于修饰实例变量或者类变量,可是不能用于修饰方法、方法参数、局部变量等,另外能够修饰的变量为null
。但synchronized
不能用于对变量的修饰,只能修饰方法或语句块,并且monitor
对象不能为null
volatile
没法保证原子性,可是synchronized
能够保证volatile
与synchronized
都能保证可见性,可是synchronized
是借助于JVM
指令monitor enter
/monitor exit
保证的,在monitor exit
的时候全部共享资源都被刷新到主内存中,而volatile
是经过lock;
机器指令实现的,迫使其余线程工做内存失效,须要到主内存加载volatile
可以禁止JVM
以及处理器对其进行重排序,而synchronized
保证的有序性是经过程序串行化执行换来的,而且在synchronized
代码块中的代码也会发生指令重排的状况volatile
不会使线程陷入阻塞,但synchronized
会