Java 关键字之volatile

  1. 先看一段代码
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
  1. 那为什么会出现这样的结果呢?因为有可能多个线程都能跑过这段代码
 if(instance == null)

即使加了同步锁,Fruit一样有可能被多次初始化。

  1. 我们把这段代码改良下
    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引用指向内存空间
  • 初始化对象
    所以,这就造成了可能有线程会拿到未完全初始化的对象,因为引发程序的崩溃。
  1. 我们再改良下上面代码
    private static volatile Fruit instance = null;

加了volatile关键的变量,这个关键字的作用之一就是禁止这个共享变量(类的成员变量,静态变量)的赋值过程指令重排序(instance赋值前面的语句都执行完成,并能获得正确结果,instance赋值后面的语句都还没执行)。因此加了volatile修饰后,上面的代码是安全的。

volatile还有一个作用,就是一旦某个共享变量被它修饰了,那么这个共享变量的变化对其他正在使用它的线程,是立即可见的。这涉及到jvm的内存模型,简单来讲就是存在一个内核空间(共享的)和线程的工作空间(独有的),每个线程在工作的时候,都去内核空间copy一份自己需要的数据,我们称之为副本。如果这些数据中有被volatile修饰的,那么这个数据一旦变化,线程工作空间里的副本就会时效,此时线程要去内核空间刷新这个数据,由此来保证数据的可见性。

如果想对volatile有更清楚的认识,可以读下这篇文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容