JMM:java内存模型
并发程序比串行程序复杂的多,其中很重要的一个原因就是并发程序中数据访问的一致性和安全性问题。如何保证在一个线程可以看到正确的数据?这在串行程序中就不是问题,但是在并发程序中,这却成了最重要的一个问题。
假如:读取了一个指,a=1,在串行程序中肯定这个a=1,但是如果这个变量是线程共享的数据,那么在并发程序中我们有可能得到的a!=1,可能等于2、3、4、5…
JMM关键的技术点是围绕着多线程的原子性、可见性、有序性来建立的。
思维导图
原子性
原子性是指操作是不可分的,要么全部一起执行,要么不执行,在java中表现为,对于共享变量的操作是不可分的,必须连续完成。比如对于a++,我们要执行三个操作:
- 读取变量a的指,假如a=1
- a的指也就是1+1,得到指2
- 将2的指赋值给变量a,此时a的指应该为2
这三个操作中任意一个操作,a的值如果被其他线程篡改了,那么都会出现我们不希望出现的结果。所以必须保证这3个操作是原子性的,在操作a++的过程中,其他线程不会改变a的值,如果在上面的过程中出现其他线程修改了a的值,在满足原子性的原则下,上面的操作应该失败。
java中实现原子操作的方法大致有2种:锁机制、无锁CAS机制。
可见性
可见性是指一个线程对共享变量的修改,对于其他的线程来说是否是可见的,可能有些人会说,修改了之后别的线程肯定可以看见呀,线程又不瞎。但是确实会有这个问题,那么为什么会发生这个问题呢,看下具体的线程、工作内存、主内存的交互关系图如下:
每个线程都有自己的工作内存,工作内存中的数据来自主内存数据的拷贝,线程对变量进行修改,线程结束后才会将变量的指写回到主内存中去,当两个线程都要修改共享变量时,就有可能发生这个问题:
假设线程1先从主内存拷贝了共享变量a,并且修改了共享变量a,但是还未将变量a写回到主内存,这时,线程2拷贝了主内存的变量a到线程2的工作内存中,之后线程1结束,将共享变量a写回到主内存,这时就会发生线程2和主内存的变量a的值不一致,问题
不可见:线程将工作内存中的共享变量修改了,还未写回至主内存,这时其他的线程对于本次修改是不可见的;而其他线程没有读取到主内存中的最新值而是使用的旧值的情况也可以列为不可见的。
共享变量可见性的实现原理:
- 线程A对共享变量的修改要被线程B看见的话需要进行以下步骤:
- 线程A在自己的主内存中将共享变量修改之后,需要将共享变量的值刷新到主内存中,线程B要把主内存中的值更新到自己的工作内存中;
关于线程可见性可以使用volatile、synchronize、锁来实现
有序性
有序性是指,程序按照代码的顺序执行。
为了性能优化,编译器和处理器进行指令重排序,有时会改变程序语句的先后顺序,常常被说起的的例子就是单例模式实现的一个例子:
public class Singleton{
private static Singleton instance;
private Singleton(){};
static Singleton getInstance(){
if(instance == null){
synchronize(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
主要是看这个:instance = new Singleton();
未被编译器优化的执行顺序:
- 分配一块内存M
- 在内存M上初始化对象Singleton
- 将M的内存地址赋值给instance变量
编译器优化后的执行顺序:
- 分配一块内存M
- 将M的内存地址赋值给instance变量
- 在内存M上初始化Singleton对象
如何复现这个问题:
假如现在2个线程,刚好执行的代码被编译器优化过,过程如下:
可以看到,线程B在if判断得到对象instance不是null,就会返回一个未被初始化的instance,可能会产生一些意想不到的错误。
如果需要实现安全的单例模式,可以将变量instance 前面加上volatile关键字,volatile可以阻止编译器指令重排序。
也可以使用静态内部类的方式实现安全的单例模式:
public class Singleton{
private Singleton(){}
private static class SingletonHandle{
priavte static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHandle.instance;
}
}