JMM
JMM又称Java Memory Model,即Java内存模型。属于计算机原理部分。谷歌公司的首席架构师Jeff Dean曾经不同的操作的相应时间做了如下统计:
1秒=1000毫秒 1毫秒=1000微秒 1微秒=1000纳秒
我们可以看到CPU执行一条指令的时间为0.6纳秒,而CPU从读取一次内存的时间为100纳秒。例如现在有一个操作a+b,那么CPU首先要花100纳秒从内存中读取a的值,再花100纳秒从内存中读取b的值。但是在执行相加的指令的时候只花了0.6纳秒。也就是说CPU绝大多数的时间都是在等待CPU去内存中读取数据。这样就出现了速度上的极大不匹配。于是为了解决这种问题,现代CPU则引入了高速缓存的概念。
可以看到每个内核除了有一级和二级缓存,并且所以核心都共享了一个三级缓存。而每级缓存的读取速度是不一样的
可以看到一级缓存读取一次的速度是1.2纳秒,二级是5.5纳秒,三级则是15.9纳秒。也就是说越接近CPU的缓存读取一次的时间越接近CPU执行一次指令所花费的时间。并且每一级的容量也是不一样的,速度越快的内存容量就越小。如果再细分的话则还有寄存器以及硬盘等
其中寄存器是在CPU内部所有的,速度也是最快的,最接近CPU执行指令的时间。在工作的时候,计算机会把将要用到的数据首先从主内存中读取到L3级内存,然后L2再从L3中读取,以此类推,直到被CPU使用。
现在CPU为了在提高速度上引入的高速缓存这样的实现,为了充分利用高速缓存,那么在Java中则提出了JMM这样的概念:
工作内存和主内存是一个抽象的概念,不是像电脑主机一样真实存在的一个主体。
可以看到JMM内存模型中的工作内存有可能包含了寄存器,高速缓存以及主内存,只不过可能百分之99都是寄存器或者高速缓存,只有百分之一是主内存。同样的堆内存也可能三者都有,只是主内存可能占百分之99,寄存器和高速缓存的部分占百分之1。
假如现在主内存中有一个变量count,因为,所以线程需要从主内存从拷贝一份变量到自己的工作内存当中。多个线程那么就每个线程就都需要去拷贝。另外JMM还规定工作内存是每个线程独有的,任何线程都不允许访问其他线程的工作内存。
JMM带来的并发安全问题
如图所示,现在存在一个count变量等于0,现在要对count进行加1操作。可以看到首先线程要从主内存中拷贝一份count的副本保存在自己线程内部。这是因为JMM规定线程是不能直接访问主内存。假如现在又来了一个线程2,那么线程2也要执行相同的操作保存一份副本在自己的工作内存中。那么两个线程都对count进行加1之后要写到主内存中。由于JMM规定线程之间的工作内存是不能够互相访问的,就导致了线程1把计算之后的结果count = 1写进主内存之后,线程2又写了一遍count = 1,从而导致最后的结果是count = 1 而不是 2 。这就是常见的多线程带来的线程数据不安全的问题。
volatile
可见性
volatile是Java中的一个关键字,用来修饰变量,以保证变量的可见性。根据上面的JMM内存模型我们得知线程之间是隔离的,彼此不能访问对方的工作内存。volatile的真正作用是当一个线程需要使用一个变量的时候强制线程去主内存中读取一次数据,并且在进行完操作之后强制的把结果写进主内存。还是上面的例子,线程A和线程B要对count进行加1。由于count现在被volatile修饰了,线程A进行了执行了一次之会被要求立即把结果写到主内存中去。而线程B要使用count的时候则需要重新去住内存中读取一份。在线程B执行加1操作的时候,由于加1操作不是一个原子操作,完全有可能在执行的时候由于上下文切换被切换出去了,而线程A又对count进行了一次累加,现在主内存中count的值为2了。当重新切换回线程B的时候,并不会强制去内存中重新读取一遍。volatile的强制读取只是在要使用变量的时候需要去主内存读取,但是在真正执行指令的时候是不能够返回读取重新执行的。造成这种问题的原因是加一操作不是一个原子操作。也就是说volatile只保证可见性,不保证原子性。也就是说如果被volatile所修饰的变量只进行读和写这样的非复合性操作的时候才有效果,例如:
public class VolatileTest {
private volatile int count;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
这个时候即使在多线程的情况下只使用volatile也能够保证正确性。
抑制指令重排序
public void test() {
int a = 0;
int b = 6;
int c = 2;
if (b == 10){
int d = c;
}
}
现在CPU在执行代码的时候并不是按照我们上述方法中一行一行的去执行。有可能一下就把前三行全部执行,也有可能会先把前三行加上 int d = c 先执行了,这是因为CPU会在自己内部开辟一个称为重排序缓存的部分,把 d = c的结果存到重排序缓存当中,当真的执行到b == 10 成立的时候则直接把重排序缓存中的结果直接使用,而不需要非得等到b == 10的时候再去执行。那么如果在一个变量的前面加上volatile关键字的时候,则不会出现这种情况。
实现原理
由volatile修饰的变量在进行写操作的时候会使用CPU提供的Lock前缀指令。lock前缀的作用一是将当前处理器缓存行的数据写回到主内存,作用二则是这个写回内存的操作会使在其他CPU中缓存了该内存地址的数据无效。
网友评论