内存模型的相关概念:
程序的执行过程为:主存->复制到cache(CPU的高速缓存)->CPU->刷新cache->回写到主存中 共享变量:被多个线程同时访问的变量 特殊情况:主存中i=0,程序A和B分别读i=0至核心1和核心2(多核CPU)的cache,进行i=i+1,则对这个共享变量虽然操作了两次,但是最后写回主存还是i=1。具体过程如下:
|-------主存-------|----核心1的cache----|----核心2的cache----| |-------i=0-------|-----i=0-----------|------i=0-----------| |-------i=0-------|-----i=1-----------|------i=1-----------| |-------i=1-------|-----i=1-----------|------i=1-----------|复制代码
这就是著名的缓存一致性问题 问题的解决方案MESI协议:当CPU写数据时,如果发现这个变量是共享变量,则通知其他的CPU设置该共享变量的缓存行为无效,当其他CPU需要读取这个变量时,发现自己的缓存中缓存该变量的缓存行是无效的,它就会从内存中读取
原子性:
要么全部执行,要么全部不执行,不会被打断。eg.对一个32位的int型变量赋值:eg.i=3分为两步:1.对低16位赋值。2.对高16位赋值。这时就必须要原子性操作,要么全部成功,要么全部不成功复制代码
可见性:
多个线程访问同一个变量的时候,一个线程修改了值,另一个线程能立即看到修改的值eg.线程1:int i = 0; i=10, 线程2:j=i。CPU1执行线程1,CPU2执行线程2当线程1将i=0读到cache中并且设置为10(线程修改了值),但是未写入主存,线程2执行j=i时还是取出主存中i=0的值(未看到最新的值)复制代码
有序性:
以下指令可能会发生指令重排序语句1:int a=10;语句2:int r=2;语句3:a=a+3;语句4:r=a*a;考虑数据依赖性之后的指令执行顺序可能是2->1->3->4复制代码
多线程下的有序性问题:
线程1:(语句1)context = loadContext(); (语句2)intited=true;线程2:(语句1)while(!inited){ sleep() } (语句2)doSomethingwithconfig(context)因为线程1的两个语句之间没有依赖,所以可能发生不恰当的指令重排列:如下:线程1先执行了语句2,导致线程2的intied误认为初始化完成,线程2跳过语句1,进行了语句2的doSomethingwithconfig(context)工作由此可见,指令重排列不会影响当个线程的执行,但会影响到程序并发的执行复制代码
java内存模型:
在JVM规范中,试图定义了一种Java内存模型,(java memeory model,JMM)来保证各个平台下的内存访问差异,以实现个平台下的JVM都能达到一致的访问内存的效果,但是JVM没有限制CPU或者寄存器,更没有禁止指令重排列,所以也会存在一致性问题。
Java内存模型规定所有的变量都是存在主存当中(类似于物理内存),每个线程都有自己的工作内存(类似于高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
原子性:java内存模型只对基本数据类型的读取和赋值(int i = 0)保证了原子性,剩下的需要由synchronized和Lock来实现
可见性:volatile修饰的变量,一旦发生修改,就会更新主存,synchronized和Lock一样可以保证可见性
synchronized和Lock可以保证在释放锁之前将会对变量的修改刷新到主存中
案例分析:
volatile的意义: 1.保证了不同线程对这个变量进行操作的可见性 2.禁止指令重排序复制代码
//线程1boolean isStop = false;while(!stop){ doSomething()}//线程2stop = true;复制代码
每个线程都有自己的工作内存,每个线程对变量的操作都要在工作内存中进行,不能直接对主存进行操作,线程之间的工作内存相互隔离线程1将isStop读取到自己的工作内存,如果线程2在自己的工作内存中对isStop进行了修改,但是线程1还是没有进行刷新,所以会一直运行下去对线程1的isStop加上volatile之后就保证了可见性:1.当线程2修改isStop时,会做两件事,一就是对isStop立即更新到主存,二就是置线程1的缓存行为无效2.当线程1再次读取isStop时,发现自己的缓存行无效,就会去读主存最新的值volatile可以保证共享变量的可见性复制代码
案例分析2:
public class ThreadVolatile { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final ThreadVolatile test = new ThreadVolatile(); for (int i = 0; i < 1000; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { test.increase(); } }).start(); } while (Thread.activeCount() > 1) { //保证前面的线程都执行完 Thread.yield(); } out.println("inc=" + test.inc); }}复制代码
输出:
inc=999452复制代码
因为自增操作不是原子性的,所以虽然保证了可见性,但还是不够,每次操作的数都会小于10000eg.假设i=10此时,线程1取出i=10,还未进行(i=i+1,写入主存)这两个操作时就阻塞了 线程2取出i=10,完成了i=i+1,写入了主存i=11 线程1执行i=i+1,写入主存i=11;volatile不可以保证原子性使用AtomicInteger,是JDK中新增的一种利用CAS锁原理实现的基本数据类型的原子操作参考下面的代码:复制代码
public class ThreadVolatile { public AtomicInteger anInt = new AtomicInteger(0); public void increase() { anInt.incrementAndGet(); } public static void main(String[] args) { final ThreadVolatile test = new ThreadVolatile(); for (int i = 0; i < 1000; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { test.increase(); } }).start(); } while (Thread.activeCount() > 1) { //保证前面的线程都执行完 Thread.yield(); } out.println("inc=" + test.anInt.get()); }}复制代码
输出:
inc=1000000复制代码
有序性:
因为volatile不可以保证变量的原子性,但是可以保证变量的可见性,那么可以部分保证有序性,如下:复制代码
//线程1:context = loadContext(); //语句1volatile inited = true; //语句2//线程2:while(!inited ){ sleep()}doSomethingwithconfig(context);复制代码
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。复制代码
使用场景:
具备条件:
1.对变量的写操作不依赖于当前值
2.该变量没有包含在其他变量的不变式中
也就是说,必须保证操作是原子性操作(i++就不可以),才能保证volatile关键字的程序在并发的时候能够正确执行
例如: 状态标记量:
volatile boolean flag = false;while(!flag){ doSomething()}public void setFlag(){ flag = true}复制代码
//使用于double check机制
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; }}复制代码