java模型规定:所有变量都要存储在主内存(Main Memory)中,每一个线程都有自己的工作内存(Working Memory),线程中的工作内存中保存了被该线程使用的变量的主内存副本拷贝,线程对变量的操作(读取,复制)都必须在工作内存进行,而不能直接读写内存中的变量。而不同线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要通过主内存完成。
学过操作系统的都应该明白,高速缓存为了缓解CPU与硬件的速度差等问题,主内存对应的是物理硬件,为了提高运行速度,虚拟机会让工作内存有限存储与寄存器和高速缓存中,因为程序运行时主要读写的工作内存。
主内存和工作内存之间有8种操作完成,虚拟机实现每个操作都是原子的,不可再分的:
- luck: 作用在主内存的变量,它把一个变量标示为一条线程独占的状态。
- unluck: 作用在主内存的变量,它把一个处于锁定状态的变量释放出来,给其他线程访问。
- read: 作用在主内存的变量,它把一个变量从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load: 作用在工作内存的变量,它把read操作从主内存中得到的变量放在工作内存变量的副本中。
- use: 作用在工作内存的变量,它把工作中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用的变量的值的字节码指令时将会执行这个操作。
- assign: 作用在工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令式执行这个操作。
- store: 作用在工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write: 作用在主内存的变量,它吧store操作从工作内存中得到的变量值放入主内存的变量中。
把一个变量从主内存复制到工作内存时,必须顺序执行read 和 load ,
把一个变量从工作内存复制到主内存时,必须顺序执行store 和 write。
但是,不需要连续执行,他们之间可以插入其他的指令,
如 访问内存中a , b 可能执行read a read b load b load a。
volatile 变量特殊规则:
这个java 提供的轻量级同步机制,很难理解。
首先,一个volatile 变量具有两种属性:
1.保证变量对所有线程的可见性,当一个线程改变了,新值对其他线程是立即可见的,而普通变量,必须将值由工作线程传递给主内存。
很多人认为被volatile就能实现 并发的一致性,但是被volatile修饰的变量运算不一定是线程安全的,网友们一般都那自增来举例,同样,我们来看一个。
package cn.pmax;
public class VolatileTest {
private static volatile int count = 0;
public static void main(String[] args) {
//开启100个线程 自增count
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(20); //延时20毫秒 增强实验效果
} catch (InterruptedException e) {
}
count++;
}
}).start();
}
while (Thread.activeCount() > 1)<span style="white-space:pre"> //让所有开启线程都结束
Thread.yield();
System.out.println(count);
}
}
可以看到 得到的结果并非是100,因为++在jvm编译后 生成了多条指令。
static int access$008();
Code:
0: getstatic #1 // Field count:I
3: dup
4: iconst_1
5: iadd
6: putstatic #1 // Field count:I
9: ireturn
我们可以看到,自增操作分成了两步(4,5)当执行到 4时 有其他线程进来操作了过期数据,使结果变小。
所以,volatile 变量职能保证可见行,但是不能保证原子行。
volatile多用于两种情况:
1.运算不依赖当前值,
2.并发时 作为 状态标签
两个常用例子:
volatile boolean asleep;
while(!asleep) {
//TODO ....
}
当asleep状态发生改变时,while语句会立即终止。
饭后继续。。。。。
2.禁止指令重排序优化。
一个因优化引发的血案
public class Singleton {
private static Singleton intance;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (intance == null) {
intance = new Singleton();
}
return intance;
}
}
因为 synchronized 加在方法上降低性能,所以有人提出了双重锁检查
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
貌似很完美的解决了性能问题,但是由于对jvm虚拟机不熟,引来了一个潜在的问题,可能导致 获得一个还没有完成的初始化对象。
我们JIT生成反汇编代码发现 new 一个对象时 分为三步,
memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance = memory; //3:设置instance指向刚分配的内存地址
由于jvm会对指令重排序,所以可能 执行完成1后 执行 3 这时候还没有初始化对象,当一个线程到达最外层的if判断时,instance不为null 直接返回了一个没有完成初始化的对象。
所以jdk 5 后给 属性加volatile 即可 防止指令重排序来解决线程安全问题。
public 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;
}
}