目标
Java内存模型的目标是定义程序中各个变量的访问规则.即虚拟机对变量在内存中存取的规则.
目的
Java内存模型的目的是屏蔽各种硬件和操作系统的内存访问差异,使得Java在各个平台下能达到一致的内存访问效果.
主内存与工作内存
- Java内存模型将规定所有变量存储于主内存,但每条线程拥有一个自己独立的工作内存.
- 工作内存中保留了该线程对于主线程中用到的变量的拷贝.(这里需要强调一点,并不是用到一个对象,就要将这个对象全部拷贝到工作内存中来,工作内存拷贝的是变量,而这些变量一遍只是一个对象的一部分).
- 工作内存彼此独立,无法相互访问
Java线程 <===>工作内存<===>Save&Load<===>主内存
进一步理解,主内存可以理解为Java堆,而工作内存可以理解为栈,但工作内存还有一部分属于高速缓存.
内存间的交互操作
共有八种操作:lock,unlock,read,load,use,assign,store,write
一下是对这些操作的限制:
- read之后必须要有load,store之后必须要有write
- assign之后必须要有store(原话是一个线程不允许丢弃最近的assign操作,这是笔者自己的理解)
- 不允许没有assign却调用store
- 如果一个变量没有被assign和load,那么就不能use
- 一个变量同时只允许一个线程对其lock,但是一个线程可以对其多次lock,但之后得进行多次unlock才能解锁
- 对一个变量执行lock操作,首先会清空该变量在工作空间中的值,在使用到这个变量前,需要对它重新调用load或assign操作
- unlock前必须lock
- 在对一个变量执行unlock操作前,需要调用store,write将它的值同步到主内存中
volatile的特殊规则
可见性
在volatile标记的变量使用前必会调用read->load->use
在volatile标记的变量赋值后,会调用assign->store->write
也就是说,被volatile标记的变量标记的对象会及时的刷新主内存中它的值,并在工作内存中使用主内存中最新的值.
但由于从use到变量真正的使用往往不是原子性操作,因此单纯的使用volatile也往往不是线程安全的
有序性
在计算机执行指令时,会对指令就行重排序,而volatile关键字能阻止这个行为。在volatile之前的操作不会再它之后执行,而在volatile之后的操作不会在它之前执行。典型的例子就是双重判断的单例模式,对instance加volatile关键字的目的就是防止对象初始化和对象赋值的重排序。
final
final与volatile相同也具备可见性和有序性。
Happens-Before规则
- 单个线程内程序顺序执行,重排序线程内不可知。
- 解锁操作先于加锁操作
- volatile的写入操作先于读取操作
- 线程启动先于线程运行
- 线程运行先于线程结束
- 对象构造函数先于对象的终结函数
- 传递性
举个例子,有一下两个线程:
线程一:
y = 1
lock M
x = 1
unlockM
线程二:
lock M
i = x
unlockM
j = y
如果线程1的unlockM先于线程2的lockM
那么我们可以推到出线程1的所有操作先于线程2;
但是如果去掉lock和unlock代码:
y = 1
x = 1
线程二:
i = x
j = y
如果x = 1 先于 i = x;我们并不能推出y = 1 先于 j = y;
再举个简单的例子,看如下代码:
new Thread(new Runnable() {
@Override
public void run() {
while (!shutdown);
System.out.println(count);
}
}).start();
Thread.sleep(100);
for (int i = 0; i < 500; i++) {
count++;
}
shutdown = true;
如果shutdown不为volatile,那么大概率这个进程永远也无法结束,因为shutdown不具备可见性。即使这个进程成功停止了,输出的count也是不确定的。
但如果shutdown是volatile,那么输出的count必定等于500,因为我们能确定对shutdown的写操作一定先于对shutdown的读操作,那么在对shutdown写操作之前的所有操作也应该先于对shutdown读操作之后的所有操作,因此,对count的写操作就会先于对count的读操作。
网友评论