Java内存模型(JMM)
JMM 可以屏蔽掉各种硬件和操作系统的内存访问差异,让Java程序在各个平台下都能达到一致的内存访问效果。
1. 主内存与工作内存
JMM规定所有的变量(包括实例变量、静态字段、构成数组对象的元素)都存储在主内存中,每个线程还有自己工作内存。线程工作内存保存了被该线程使用的变量的主内存的副本拷贝(不会拷贝整个对象,可能拷贝的是对象的引用、所对象访问的字段),线程对变量的操作都是在工作内存中进行,线程之间的值传递需要通过主内存完成。
2. 内存间交互操作
工作内存和主内存间的交互操作:一个变量如何从主内存拷贝到工作内存,在如何从工作内存同步到主内存。
JMM中定义了8中原子性操作来完成。
操作 | 含义 | 作用区域 | 作用 |
---|---|---|---|
lock | 锁定 | 主内存变量 | 把一个变量标识为一条线程独占状态 |
unlock | 解锁 | 主内存变量 | 把一个处于锁定的变量释放出来 |
read | 读取 | 工作内存变量 | 把一个变量从主内存传输到线程的工作内存 |
load | 载入 | 工作内存变量 | 把read操作的变量值放入工作内存副本中 |
`use | 使用 | 工作内存变量 | 把工作内存中的一个变量的值传递给执行引擎,当JVM遇到一个需要变量的值得字节码指令时将会执行这个操作 |
assign | 赋值 | 工作内存变量 | 把从执行引擎接受到的值给工作内存的变量,当JVM遇到一个给变量赋值的额字节码指定时执行该操作 |
store | 存储 | 工作内存变量 | 把工作内存中的一个变量的值传递到主内存中 |
write | 写入 | 主内存变量 | 把store 操作传递的值放入工作内存的变量中 |
- 变量复制主内存->工作内存:顺序执行
read
、load
- 变量同步工作内存->主内存:顺序执行store、
write
上面两过程不要必须连续执行,只要顺序执行,且每个操作不得单独出现。
3. Volatile登场
- 可见性
volatile
修饰的变量,对所有的线程是可见的。即当一个线程修改了该变量,其他线程都是立即可见的。但是,并不表示volatile
变量是支持线程安全的。
public static volatile int count = 0;
private static final int THREAD_COUNT = 30;
public static void increase(){
count++;
}
public static void main(String[] args){
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0 ;i<THREAD_COUNT;i++){
threads[i]= new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<1000;i++){
increase();
}
}
});
threads[i].start();
}
//等待所有线程执行完毕
while (Thread.activeCount()>1){
Thread.yield();
}
System.out.println(count);
}
在这里定义了30个线程去累加count,每个线程加1000,理想在并发状态下得到的应该是30000
,实际输出来看下
实际得到的数值会比预计的要小。
由于这里使用
count++
来累加改变这个volatile
变量,而自增操作并不是原子性的,即使count
变量是所有线程可见,但不能保证在并发执行运算的时候,非原子性操作一定是读取的和和主内存变量同步的值。
- 禁止指令重排优化
普通变量仅仅能保证在执行该方法的过程中所有依赖赋值的地方能得到正确的结果,但不能保证程序一定按照代码的顺序执行
看个伪代码
//定义一个标识
boolean flag = false;
//业务代码A
service.changeState();
//业务代码执行完,改变flag
flag = true;
//.....
//当flag为true时,执行另一段业务B
while(flag){
//dosomething
}
由于系统有指令优化重排,获取flag=true
可能会被提前执行,也就是在业务A
没有执行情况下,就已经改变了flag。实际上我们想要的是在业务A
执行完之后在区执行flag=true
。
//定义一个标识
volatile boolean flag = false;
//业务代码A
service.changeState();
//业务代码执行完,改变flag
flag = true;
//.....
//当flag为true时,执行另一段业务B
while(flag){
//dosomething
}
加了volatile
就可以让代码按序执行
4. 先行发生原则(happents-before)
先行发生是JMM中定义的两项操作之间的偏序关系,如果说操作A先行于操作B发生,其实就是说在操作B发生之前,操作A产生的影响能被操作B观察到。
这里的影响
包括修改了内存中共享变量的值、发送了消息、调用了方法等。
//thread A 执行
i=1;
//thread B 执行
j=i;
//thread C 执行
i=4;
假设线程A会先于B执行,那么当只执行线程A、B时,A永远先于B执行,所以j
一定等于1。若在A、B之间加入线程C执行,并没有规定C会先于B发生,所以此时j
的值就会不确定,线程不安全。
下面介绍下Java中