简介
定义
**JMM(JAVA内存模型)**本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
运行过程
- JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(栈空间),工作内存是每个现成的私有数据区域
- Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问
- 线程对变量的操作(读取赋值)必须在工作内存中进行,首先要将变量从主内存拷贝到线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存
- 不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成
三特征
参考:
原子性
- 定义
原子性操作就是指这些操作是不可中断的,要做一定做完,要么就没有执行,也就是不可被中断。 - 例子
- 解决:在java中提供了两个高级的字节码指令monitorenter和monitorexit,使用对应的关键字Synchronized来保证代码块内的操作是原子的
可见性
- 定义
可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改 - 解决方法
- Java中可以使用volatile关键字来保证多线程操作时变量的可见性
- volatile的功能是被其修饰的变量在被修改后可以立即同步到主内存,而被其修饰的变量在每次使用之前都会从主内存刷新
- 除此之外,synchronized和final两个关键字也可以实现可见性 。volatile也可以看作是轻量级的锁,在其内部使用了Lock指令来解决可见性问题
有序性
- 定义
JMM是允许编译器和处理器对指令重排序的,但是规定了 as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变 - 多线程出错例子
/*
如果这个类的writer()和reader()方法是在不同的线程中运行的。
那么writer()中的方法可能会被重排序为flag= true先执行。
这个时候如果被中断,换到执行reader()的线程执行,flag为true,进入if判断就会自然认为a = 1;
但是这个时候a还是0。这里大概就能理解重排序带来的问题了
*/
class OrderExample{
int a = 0;
boolean flag = false;
public void writer(){
a = 1;
flag = true;
}
public void reader(){
if(flag){
int i = a + 1;
}
}
- 解决方法:
在Java中,可以使用synchronized和volatile来保证多线程之间操作的有序性。只是实现方式有所区别: volatile关键字会禁止指令重排,synchronized关键字保证同一时刻只允许一个线程的操作 - 解决例子
/*
使用volatile来标示flag,就能解决上面说到的可见性问题
这种对变量的读写操作,标记为 volatile可以保证修改对线程立刻可见
比 synchronized, Lock有一定的效率提升
*/
class OrderExample{
int a = 0;
volatile boolean flag = false;
public void writer(){
a = 1;
flag = true;
}
public void reader(){
if(flag){
int i = a + 1;
}
}
}
JMM数据原子操作
- read(读取):从主内存读取数据(还未写入工作内存)
- load(载入):将主内存读取到的数据写入工作内存
- use(使用):从工作内存读取数据来计算
- assign(赋值):将计算好的值重新赋值到工作内存中
- store(存储):将工作内存数据写入主内存(还未写入主内存)
- write(写入):将store过去的变量值赋值给主内存中的变量
- lock(锁定):将主内存变量加锁,标识为线程独占状态
- unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
read和load
store和write
必须成对出现