一、前言
Java作为一门后端语言,服务器端难免涉及到并发的问题,之前接触并发同步等问题是从操作系统课上学到的,在学习了相关知识后认识到无论是OS的进程调度还是JVM的线程调度都有相同之处。
本文以《深入理解Java虚拟机》为基础,在书的基础上加了一些个人心得(真的是本神书)
回忆一下在OS中存储结构:
由于CPU和存储器的速度的不匹配,所以在CPU和主存中间加了高速缓冲器(Cache)。
Cache中的内容是主存中的一个副本,于是就会产生Cache中的内容和主存中的内容不一致的情况,为了解决这种不一致,于是就有了不同的策略,例如:
- write through(写通):每次CPU修改了cache中的内容,立即更新到内存
- write back(写回):每次CPU修改了cache中的数据,不会立即更新到内存,而是等到cache line在某一个必须或合适的时机才会更新到内存中;
同时在Java虚拟机中,也有自己的抽象出来的内存模型
二、Java内存模型
Java内存模型分为主内存(Main Memory)和工作内存(Working Memory)
所有的变量都存储在主内存中
每个线程有自己的工作内存,保存了该线程使用的变量的主内存的副本,线程对变量的所有的操作都必须在自己的工作内存中进行
类似于OS中的Cache中是主存的副本,要保证二者的一致性,Java规定了以下的原子操作,在主内存和工作内存中进行变量的传递
- lock(锁定):作用于主内存变量,把该变量表示为一个线程独占
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放,可以被其他线程锁定
- read(读取):作用于主内存变量,把一个变量从主内存传输到工作内存,以便后面的load使用
- load(载入):作用于工作区变量,把read操作从主内存得到的变量值放入工作内存的变量副本中
- use(使用):作用于工作区变量,当虚拟机遇到一个需要使用变量的值得字节码指令时会执行这个操作
- assign(赋值):作用于工作内存变量,每当虚拟机给一个 变量的赋值的字节码指令时执行这个指令
- store(存储):作用于工作内存变量,把工作内存中一个变量的值传送到主内存中,供后面write使用
- write(写入):作用于主内存变量,把store操作从工作内存中取到的变量的值入主内存的变量中
指令重排(Instruction Reorder):
处理器在执行每一条指令的顺序进行调整,但是调整后的结果与顺序执行的结果是一致的。
例如:把一个变量从主内存复制到工作内存,那么就要顺序执行read和load操作,指令重排可能会在read和load中间插入其他指令,但是保证read在load前面执行。
三、Volatile
Volatile关键字提供最轻量级的同步机制,正确的理解Volatile关键字关键是理解可见性 和禁止指令重排优化。
3.1 可见性
可见性:当一个线程修改了Volatile变量,新的值对于其他线程可以立即得知
这是因为当一个线程在自己的工作内存中修改了Volatile变量,会立即同步到主内存,这时,其他线程想使用Volatile变量时,必须从主内存中read load,于是就保证了所有线程看到的Volatile变量都是一致的。
但这并不意味着Volatile变量一定是线程安全的
例如:
public static volatile int count = 0;
// 3个不同线程中执行10次
count++;
在实际的执行中count++最后的值并不等于30,而是会小于30
这是因为count++运算并不是一个原子操作
因为count++翻译成指令时,要先从主内存中read load count 变量到工作内存,然后在工作内存中对count进行自增,然后在store write进主内存
Volatile只保证读取和写回的时候是原子操作,但是读取到工作内存中count自增时,其他线程也有可能在进行自增,导致线程中的count会发生过期,此时写回的值也是过期的了
如果要Volatile变量保证可见性,必须准守下面两条规则
- 运算结果不依赖当前值(例如count++不符合) 或者 只有单一线程修改变量的值
- 变量不需要与其他状态变量共同参与不变约束
例如下面的例子是Volatile的正确使用姿势:
Volatile boolean flag;
public void shutdown() {
flag = ture;
}
public void doWork() {
while (!flag){
// do something;
}
}
3.2 禁止指令重排优化
被Volatile修饰的变量,还暗中规定了执行的顺序,防止指令重排时对逻辑产生错误。
例:一个产生单例的方法
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if(null == instance) {
synchronized (Singleton.class){
if (null == instance){
instance = new Singleton();
}
}
}
return instance;
}
}
3.3 总结
总结来说,Volatile的可见性和禁止指令重排可以总结为下面三条原则:
- 每次使用Volatile变量必须先从主内存中更新最新的值,用于保证看见其他线程对该值的最新之
- 每次修改Volatile变量必须立刻同步到主内存,保证其他线程可以看见自己的修改
- Volatile变量不会被指令重排优化,必须保证代码的执行顺序和程序的顺序相同
四、内存模型的特点
Java内存模型中正确的处理并发情况,就必须从下面三个内存模型的特点入手,用不同的操作保证这些特点的正确。
4.1 原子性(Atomicity)
内存模型中的基本的原子操作是上面read load等操作。
如果在更大的范围内要保证原子性,在Java语言的层面使用synchronized关键字,对应着下层字节码指令的monitorenter和monitorexit,即最底层的lock和unlock原子操作。
4.2 可见性(Visibility)
可见性值各个线程都能看到变量的最新值。
内存模型通过修改后立刻同步到主内存,读取时先从主内存更新变量值来保证可见性。
Java中可以使用Volatile final synchronized实现可见性
synchronized原理:在对一个变量执行unlock之前,必须把此变量同步回主内存
final原理:final变量一旦赋值,不予许修改,并且在其他线程就能看到final变量的值
4.3 有序性(Ordering)
Java中有序性:本线程中的所有的操作都是有序的,在另一个线程中看本线程,操作都是无序的。
Java中使用Volatile synchronized解决有序性问题
Volatile:本身含有禁止指令重排的含义
synchronized:同一时刻只能有一个线程对其进行lock
综上所述,可以发现synchronized可以解决以上所有的问题,但是遇到同步问题就盲目的使用synchronized会有比较大的性能影响,所以选择合适的同步机制,会在程序优化更加合理。