简介《二狗》:二胖,你昨天请假去面试了吗?打架,然后才能静下心来好好学习。”二狗:“那被打的呢?你知道你是什么水平,你傻笑。“二胖”:基础太差,要求回去等通知。我得好好学习了,别跟你废话了。”二狗:“问你什么问题,把你炸成这样?让我一起为复赛做好准备。“二胖”:嗯,既然你这么好奇,那我就给你大概说说吧。您可以移到小板凳上并小心。我要开始我的表演了。接下来,二胖的第一面开始了。《采访者》:二胖是吧?让我先自我介绍一下。“二胖”:好的,我叫二胖,长沙人,今年25岁,从事java开发快3年了,现在是高级“java”》XX公司XX事业部开发工程师,主要负责XX系统。....《面试官》:好的,我看到你的简历上说你精通并发编程。你能告诉我你在并发编程中知道哪些关键字吗?”二胖子:“这不是考我synchronized和volatile的。我很擅长这个。我是故意记下来的。synchronized是java提供的关键字。主要可以保证原子性和顺序。它的底层主要是通过Monitor来实现的。volatile也是java的关键字,主要作用是保证可见性。...这里省略1000字。《面试官》:八股文你背得很好。说了这么多,还是来试试看,写一个单例的双重检查锁(dcl)给我看看。“二胖:”他从屁股口袋里掏出一支笔,默写了起来。《面试官》:您提到了volatile关键字和synchronized关键字。synchronized可以保证原子性、顺序和可见性。但是volatile只能保证顺序和可见性。那么,我们再来看看双重检查锁的单例实现。synchronized都用过了,为什么还要volatile呢?这个volatile能去掉吗?”采访者:“今天的采访就到这里吧。如有消息,工作人员稍后会与您联系。谢谢你今天来面试。二胖很郁闷,为了这个问题又去谷歌了。关于“stackoverflow”也有这个问题。看来这个问题不是只有我一个人不知道吗?看来面试不委屈了。“以上故事纯属虚构,如有雷同,请以本文为主。”synchronized的顺序呢?先看看没有volatile修饰的单例:{singleton=newSingleton();}}}returnssingleton;}}上面的代码是不是看起来没问题,首先我们来看看这行代码做了什么singleton=newSingleton()我们可以将上面的过程简化为3步骤:①“JVM”为对象分配一块内存M。②在内存M上初始化对象。③将内存M的地址复制到单例变量。这一步有两个执行顺序,可以执行根据“①②③”或“①③②”。当我们按照“①③②”的顺序执行时,假设有两个线程ThreadA和ThreadB同时请求Singleton.getSingleton方法:一般情况下,“第一步:"ThreadA按照"①②③"的顺序进入第8步。line,执行singleton=newSingleton()初始化对象(参照对象初始化流程“①②③”),执行完毕。"Step2:"ThreadB进入第5行判断单例不为空(第一步已经初始化),直接返回单例**Step3:**获取这个对象做其他操作。所以看起来没有问题。那么,如果对象是按照“①③②”的步骤初始化的,我们再来看一下:“步骤1:”ThreadA进入第8行,执行singleton=newSingleton()并执行。①JVM为对象分配一块内存M。③将内存地址复制到单例变量中。《第2步:》此时ThreadB直接进入第5行,发现单例不再为空,则直接跳转到第12行获取单例返回执行操作。这时候ThreadB得到的单例对象是一个半成品对象,因为这个对象还没有初始化(“②还没有执行”)。“第三步:”因此,ThreadB获取到的对象在执行该方法时,可能会出现异常。至于为什么会这样?《Java 并发编程实战》提到会出现synchronizednon-volatileDCL(doublechecklock)的情况:线程可能会看到引用的当前值,但是对象的状态值确实很少失效,这意味着一个线程可以看到对象处于无效或错误状态。说白了ThreadB可以获得一个已经有引用但还没有分配内存资源的对象。如果要解决按照①②③的顺序创建对象的问题,其实只要在第二行加上volatile修饰即可解决指令重排。“synchronized不是保证顺序吗?volatile的顺序?synchronized不能保证指令的重排吗?”如何定义顺序?《深入理解Java虚拟机第三版》提到Java程序中的自然顺序可以用一句话来概括:如果在这个线程中观察,所有的操作都是自然有序的。如果一个线程观察另一个线程,则所有操作都是无序的。前半句是指“在线程中出现串行的语义”,后半句是指“指令重排”和“工作内存与主存同步延迟”的现象。“synchronized”的顺序是两个持有同一个锁的synchronized块只能顺序进入,即加锁的内容必须被多个线程依次执行,但是内部同步代码还是会重新排序,使得顺序可见块之间。“volatile”的顺序是通过插入内存屏障来保证指令按顺序执行的。在执行之前的指令之前,将不会有后续指令运行。就是保证编译器在优化的时候不会把指令弄乱。“同步不保证指令重排。”最后,由于自己的无知和知识的匮乏,难免有错误。如果您发现错误,请留言指出给我,我会改正。本文转载自微信?「javafinance」,可关注下方二维码。转载本文请联系java财经公众号。
