本博客为《深入理解java虚拟机》的学习笔记,所以大部分内容来自此书,另外一部分内容来自网络其他博客和源码分析。
主要内容探讨以下问题:
Ø Java内存模型、协议、规则。
Ø volatile的可见性和禁止指令重排序是什么意思?
Ø Synchronized是如何做到线程安全的?
Ø 先行发生原则。
一 Java内存模型
1 模型
Java内存逻辑模型如下:
所有变量都存储在主内存中。
每个线程都有自己的工作内存,工作内存中保存了线程使用到的主内存中变量的副本。
线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接操作主内存。
不同线程之间无法访问对方的工作内存。
线程之间的值传递均需通过主内存来完成。
2 协议
操作 |
解释 |
作用域 |
说明 |
lock |
锁定 |
主内存 |
把一个变量表示为一个线程独占的状态 |
unlock |
解锁 |
主内存 |
把一个变量从线程独占的状态释放出来,释放后的变量才能被其他线程锁定 |
read |
读取 |
主内存 |
把一个变量从主内存传输到工作内存中 |
load |
载入 |
工作内存 |
把read操作的变量值放入工作内存的变量副本中。 |
use |
使用 |
工作内存 |
把一个工作内存的变量传递给执行引擎,当虚拟机遇到一个需要使用变量值的字节码指令时会执行此操作 |
assign |
赋值 |
工作内存 |
把从执行引擎收到的值赋给工作内存的变量,当虚拟机遇到一个给变量赋值的字节码指令时会执行此操作。 |
store |
存储 |
工作内存 |
把一个变量的值传到主内存中 |
write |
写入 |
主内存 |
把store操作的值放到主内存的变量中 |
如果需要将一个变量从主内存复制到工作内存,就需要顺序的执行read、load;如果需要讲一个变量从工作内存写回到主内存,就需要顺序的执行store、write。Java内存模型要求了这两对命令的顺序,但不要求其连续,即在read和load之间、store和write允许插入其他指令。
3 规则
不允许read和load、store和write单独出现。即不允许一个变量从主内存读取了但工作内存不接受的情况;不允许从工作内存回写了但主内存不接受的情况。
不允许一个线程丢弃掉它最近的assign操作。即变量在工作内存中修改之后,必须同步回主内存中。
不允许一个线程无原因的把数据从线程的工作内存同步回主内存。即对变量没有执行assgin操作则不能回写到主内存。
一个新的变量只能在主内存中创建,不允许在工作内存中直接使用一个违背初始化过的变量。即对一个变量use前必须load;对一个变量store前必须assign。
一个变量在同一时刻只允许一个线程对其进行lock操作,但一个线程可以多次执行lock操作,多次执行lock操作以后,只有执行相同次数的unlock,变量才被解锁。
如果一个线程没有被lock操作锁定,那么不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁住的变量。
对一个变量执行unlock操作前,必须先把此变量同步回主内存,即先执行store、write操作。
从上面的规则我们可以看到:因为一个变量同一时刻只有一个线程能对其进行lock操作,在unlock前必须将变量同步会主内存,所以使用lock可以保证并发情况下数据安全。
4 long和double的非原子协定
虚拟机允许没有被volatile修饰的64位数据的多些操作划分成2次32位操作进行,即允许虚拟机实现对64位的long、double的read、load、store、write不保证原子性,即long和double的非原子协定。
目前商用虚拟机基本上都对long、double保证原子操作。
二 volatile
1 volatile变量的特性
使用volatile修饰的变量具有两种特性:可见性、禁止指令重排序优化。
1) 可见性
可见性指一个线程修改了这个变量值,新值对其他线程来说是可以立即得知的;普通变量做不到这一点。注意这一点并不意味着使用volatile修饰的变量是线程安全。
2) 禁止指令重排序优化
普通的变量仅仅保证在执行过程中,所有依赖赋值结果的地方都能获取正确的结果,而不保证变量赋值操作的顺序与代码中的执行顺序一致,这就是java内存模型中的“线程内表现为串行的语义”;而使用volatile可以实现此点。
单例模式下,如果不使用volatile修饰,通过双重检查锁创建对象,并发场景中可能出现问题,具体见后面的分析。
2 volatile变量的特殊规则
说明:因为觉得原文中对于volatile规则的描述不好理解,所以我在这里换了一种描述方式,所以如果发现这里的描述和虚拟机规范不同,请不必疑惑。
假设T表示一个线程,V、W表示两个volatile类型的变量,那么拥有以下规则:
Ø 每次使用volatile修饰的变量前,必须先从主内存中获取最新的值
线程T对变量V的use动作和线程T对变量V的read、load的动作可以认为是相关联的,必须连续一起出现。即线程T对V的前一个动作是load时,线程T才能对变量V执行use操作;如果线程T对V的后一个动作是use时,线程T才能对变量V执行load操作。
此规则要求在工作内存中,每次使用V前必须先从主内存中刷新最新的值,用于保证能看到其他线程对变量V修改后的值。
Ø 每次使用volatile修饰的变量后,必须立即同步回主内存
线程T对变量V的assign动作和线程T对变量V的store、write的动作可以认为是相关联的,必须连续一起出现。即线程T对V的前一个动作是assign时,线程T才能对变量V执行store操作;如果线程T对V的后一个动作是store时,线程T才能对变量V执行assign操作。
此规则要求在工作内存中,每次使用V后必须立即同步回主内存,用于保证其他线程能看到当前线程对变量V的值所做的修改。
Ø 代码执行顺序和程序的顺序相同
假定动作UV是线程T对变量V执行的use动作,动作RV是与之相关联的read动作;假定动作UW是线程T对变量W的use动作,动作RW是与之相关联的read动作;如果UV先于UW,那么RV先于RW。
假定动作AV是线程T对变量V执行的assign动作,动作WV是与之相关联的write动作;假定动作AW是线程T对变量W的assign动作,动作WW是与之相关联的write动作;如果AV先于AW,那么WV先于WW。
此规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同。
3 示例
a) volalite修饰的变量不是线程安全的
以下示例代码输出的结果不会为100000;基本上都会比此值略小。
public class VolaliteTester {
private static volatile int value;
private static void inc() {
value++;
}
public staticvoid main(String[] args) {
int threadCount= 10;
final int times = 10000;
Thread[] threads= new Thread[threadCount];
for (int i = 0; i < threadCount;i++) {
Threadthread = new Thread(newRunnable() {
@Override
public void run() {
for (int j = 0; j < times;j++) {
inc();
}
}
});
threads[i] = thread;
thread.start();
}
if (Thread.activeCount() >1) {
Thread.yield();
}
System.err.println("value=" + value);
}
}
b) 双重检查锁失效
单例模式下创建实例对象时,可能出现双重检查锁失效的情况,即以下示例代码可能会创建多个实例instance对象。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
super();
}
public staticSingleton getInstance() {
if (instance == null){
synchronized(Singleton.class) {
// 如果在等待synchronized结束前已经有线程创建Instance则直接忽略。
if(instance == null){
instance = newSingleton();
}
}
}
return instance;
}
}
在执行instance = new Singleton()语句时,实际上分为分配内存、调用构造函数、instance指向分配的内存地址三个步骤。如下伪代码所示:
memery =allocate(); //为对象分配内存
ctorSingleton(memery); //调用构造函数实例化对象
instance = memery; //将instance指向新分配的内存
但是实际上有些虚拟机进行指令重排序以后会变成如下顺序(虚拟机的内存模型以及协议规则均没有限制不能进行这种操作)。
memery =allocate(); //为对象分配内存
instance = memery; //将instance指向新分配的内存,注意此时instance为not null,但是此时对象并未实例化,如果此时执行非空判断,将返回true。
ctorSingleton(memery); //调用构造函数实例化对象
三 原子性、可见性、有序性
1 原子性(Atomicity)
java内存模型直接对变量的read、load、use、assign、store、write操作的原子性(long、double的非原子协定基本是例外,但基本不会遇到)
通过synchronized关键字实现lock、unlock操作,保证同一时间段内只有一个线程访问同步快,所以可以实现代码块的原子性。
2 可见性(Visibility)
java内存模型是通过变量使用前从主内存读取、变量修改后将值同步回主内存来实现可见性的。
volalite的可见性是由:修改后的新值立即同步到主内存,使用前立即从主内存中读取新值这个规则决定的。volatite保证了多线程操作时变量的可见性,而普通变量却不行。
Synchronized的可见性是由:在unlock前必须将变量先同步到主内存这个规则决定的。
final的可见性是由:在构造函数中初始化后,不会将this的引用传递出去,以后将无法修改此值这个规则决定的。
3 有序性(Ordering)
如果在本线程内观察,所有操作都是有序的;如果在一个线程观察另外一个线程,所有操作都是无序的。前半句指:线程内变形为串行的语义;后半句指:指令重排序闲现象和工作内存与主内存同步延迟现象。
Volatile本身就有禁止指令重排序的语义,所以可以保证有序性。
Synchronized的有序性是由:同一时刻只允许一个线程对其进行lock操作这个规则决定的,这决定了synchronized的语句块只能串行进入,所以可以保证有序性。
四 先行发生原则
以下是java内存模型提供的“天然”的先行发生关系,这些先行发生关系不需任何同步协助就已经存在。如果两个操作之间的关系不在此列,并且无法通过这些规则推导出来,那么他们就没有顺序保证,虚拟机可能对他们随意的进行重排序。
1. 程序次序规则(Program Order Rule)
在一个线程中,按照代码顺序,书写在前的操作先行发生于书写在后的操作。确切的说,应该是控制流顺序而不是书写顺序,例如分支、循环机构。
2. 管程锁定规则(Monitor Lock Rule)
一个unlock操作先行发生于后面对同一个锁的lock操作。后面值的是时间上的先后顺序。
3. volatile变量规则(Volatile Rule)
对一个volatile变量的写操作先行发生于后面对这个变量的读操作。。后面值的是时间上的先后顺序。
4. 线程启动规则(Thread Start Rule)
Thread对象的start方法先行发生于对此线程的每一个动作。
5. 线程终止规则(Thread Termination Rule)
线程中的所有操作都先行发生于对此项承德终止检测.可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到县城已经终止执行。
6. 线程中断规则(Thread Interruption Rule)
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。可以通过Thread.interrupted()方法检测到是否发生中断。
7. 对象终结规则(Finalizer Rule)
一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize()方法的开始。
8. 传递性(Transitivity)
如果操作A先行发生于操作,操作B先行发生于操作C,那么可以得出操作A先行发生于操作C。
五 参考博客
对象创建过程见博客
双重检查锁更多分析见博客
http://blog.csdn.net/zhangzeyuaaa/article/details/42673245