- 先看一段代码
public class MainActivity extends AppCompatActivity {
private static Fruit instance = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sThis = this;
for (int i=0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
getInstance();
}
}).start();
}
}
public static Fruit getInstance(){
if(instance == null){
synchronized (MainActivity.class){
instance = new Fruit();
}
}
return instance;
}
static class Fruit{
public Fruit(){
Log.d("MainActivity","create Fruit");
}
}
}
大家猜下执行结果。
这个执行结果很可能不是打印一次"create Fruit",我本次测试结果如下:
2019-12-28 12:26:22.844 21686-22375/com.debug.pluginhost D/MainActivity: create Fruit
2019-12-28 12:26:22.849 21686-22376/com.debug.pluginhost D/MainActivity: create Fruit
- 那为什么会出现这样的结果呢?因为有可能多个线程都能跑过这段代码
if(instance == null)
即使加了同步锁,Fruit一样有可能被多次初始化。
- 我们把这段代码改良下
public static Fruit getInstance(){
if(instance == null){
synchronized (MainActivity.class){
if(instance == null){
instance = new Fruit();
}
}
}
return instance;
}
加了一个double-check,双重检测,看下执行结果
2019-12-28 12:40:38.830 21952-23096/com.debug.pluginhost D/MainActivity: create Fruit
现在结果是执行正常了。但是还有一个隐患。那就是某个线程可能拿到未完全初始化的对象,这意味着如果该线程使用该对象,极有可能引发崩溃。那为什么可能有线程会拿到未被完全初始化的对象呢?
因为虽然我们看到初始化过程只有下面这一行代码
instance = new Fruit();
但是对于jvm来说不是,这个过程大致对应一下几条指令
- 申请内存空间
- 初始化对象
- 将instance引用指向内存空间
如果jvm是按照这个流程执行的指令,那么上面的代码也没什么问题。但是实际上jvm执行的指令顺序很可能是这样的 - 申请内存空间
- 将instance引用指向内存空间
- 初始化对象
所以,这就造成了可能有线程会拿到未完全初始化的对象,因为引发程序的崩溃。
- 我们再改良下上面代码
private static volatile Fruit instance = null;
加了volatile关键的变量,这个关键字的作用之一就是禁止这个共享变量(类的成员变量,静态变量)的赋值过程指令重排序(instance赋值前面的语句都执行完成,并能获得正确结果,instance赋值后面的语句都还没执行)。因此加了volatile修饰后,上面的代码是安全的。
volatile还有一个作用,就是一旦某个共享变量被它修饰了,那么这个共享变量的变化对其他正在使用它的线程,是立即可见的。这涉及到jvm的内存模型,简单来讲就是存在一个内核空间(共享的)和线程的工作空间(独有的),每个线程在工作的时候,都去内核空间copy一份自己需要的数据,我们称之为副本。如果这些数据中有被volatile修饰的,那么这个数据一旦变化,线程工作空间里的副本就会时效,此时线程要去内核空间刷新这个数据,由此来保证数据的可见性。
如果想对volatile有更清楚的认识,可以读下这篇文章
。