java内存模型(JMM)和happens-beforejava
咱们知道java程序是运行在JVM中的,而JVM就是构建在内存上的虚拟机,那么内存模型JMM是作什么用的呢?git
咱们考虑一个简单的赋值问题:github
int a=100;
JMM考虑的就是什么状况下读取变量a的线程能够看到值为100。看起来这是一个很简单的问题,赋值以后不就能够读到值了吗? 数组
可是上面的只是咱们源码的编写顺序,当把源码编译以后,在编译器中生成的指令的顺序跟源码的顺序并非彻底一致的。处理器可能采用乱序或者并行的方式来执行指令(在JVM中只要程序的最终执行结果和在严格串行环境中执行结果一致,这种重排序是容许的)。而且处理器还有本地缓存,当将结果存储在本地缓存中,其余线程是没法看到结果的。除此以外缓存提交到主内存的顺序也肯能会变化。缓存
上面提到的种种可能都会致使在多线程环境中产生不一样的结果。在多线程环境中,大部分时间多线程都是在执行各自的任务,只有在多个线程须要共享数据的时候,才须要协调线程之间的操做。安全
而JMM就是JVM中必须遵照的一组最小保证,它规定了对于变量的写入操做在何时对其余线程是可见的。多线程
上面讲了JVM中的重排序,这里咱们举个例子,以便你们对重排序有一个更深刻的理解:并发
@Slf4j public class Reorder { int x=0, y=0; int a=0, b=0; private void reorderMethod() throws InterruptedException { Thread one = new Thread(()->{ a=1; x=b; }); Thread two = new Thread(()->{ b=1; y=a; }); one.start(); two.start(); one.join(); two.join(); log.info("{},{}", x, y); } public static void main(String[] args) throws InterruptedException { for (int i=0; i< 100; i++){ new Reorder().reorderMethod(); } } }
上面的例子是一个很简单的并发程序。因为咱们没有使用同步限制,因此线程one和two的执行顺序是不定的。有可能one在two以前执行,也有可能在two以后执行,也可能二者同时执行。不一样的执行顺序可能会致使不一样的输出结果。app
同时虽然咱们在代码中指定了先执行a=1, 再执行x=b,可是这两条语句其实是没有关系的,在JVM中彻底可能将两条语句重排序成x=b在前,a=1在后,从而致使输出更多意想不到的结果。函数
为了保证java内存模型中的操做顺序,JMM为程序中的全部操做定义了一个顺序关系,这个顺序叫作Happens-Before。要想保证操做B看到操做A的结果,无论A和B是在同一线程仍是不一样线程,那么A和B必须知足Happens-Before的关系。若是两个操做不知足happens-before的关系,那么JVM能够对他们任意重排序。
咱们看一下happens-before的规则:
注意,这里的操做A在操做B以前执行是指在单线程环境中,虽然虚拟机会对相应的指令进行重排序,可是最终的执行结果跟按照代码顺序执行是同样的。虚拟机只会对不存在依赖的代码进行重排序。
锁咱们你们都很清楚了,这里的顺序必须指的是同一个锁,若是是在不一样的锁上面,那么其执行顺序也不能获得保证。
原子变量和volatile变量在读写操做上面有着相同的语义。
上面的规则2很好理解,在加锁的过程当中,不容许其余的线程得到该锁,也意味着其余的线程必须等待锁释放以后才能加锁和执行其业务逻辑。
4,5,6,7规则也很好理解,只有开始,才能结束。这符合咱们对程序的通常认识。
8的传递性相信学过数学的人应该也不难理解。
接下来咱们重点讨论一下规则3和规则1的结合。讨论以前咱们再总结一下happens-before究竟是作什么的。
由于JVM会对接收到的指令进行重排序,为了保证指令的执行顺序,咱们才有了happens-before规则。上面讲到的2,3,4,5,6,7规则能够看作是重排序的节点,这些节点是不容许重排序的,只有在这些节点之间的指令才容许重排序。
结合规则1程序顺序规则,咱们获得其真正的含义:代码中写在重排序节点以前的指令,必定会在重排序节点执行以前执行。
重排序节点就是一个分界点,它的位置是不可以移动的。看一下下面的直观例子:
线程1中有两个指令:set i=1, set volatile a=2。
线程2中也有两个指令:get volatile a, get i。
按照上面的理论,set和get volatile是两个重排序节点,set必须排在get以前。而依据规则1,代码中set i=1 在set volatile a=2以前,由于set volatile是重排序节点,因此须要遵照程序顺序执行规则,从而set i=1要在set volatile a=2以前执行。一样的道理get volatile a在get i以前执行。最后致使i=1在get i以前执行。
这个操做叫作借助同步。
咱们常常会用到单例模式来建立一个单的对象,咱们看下下面的方法有什么不妥:
public class Book { private static Book book; public static Book getBook(){ if(book==null){ book = new Book(); } return book; } }
上面的类中定义了一个getBook方法来返回一个新的book对象,返回对象以前,咱们先判断了book是否为空,若是不为空的话就new一个book对象。
初看起来,好像没什么问题,可是若是仔细考虑JMM的重排规则,就会发现问题所在。
book=new Book()其实一个复杂的命令,并非原子性操做。它大概能够分解为1.分配内存,2.实例化对象,3.将对象和内存地址创建关联。
其中2和3有可能会被重排序,而后就有可能出现book返回了,可是尚未初始化完毕的状况。从而出现不能够预见的错误。
根据上面咱们讲到的happens-before规则, 最简单的办法就是给方法前面加上synchronized关键字:
public class Book { private static Book book; public synchronized static Book getBook(){ if(book==null){ book = new Book(); } return book; } }
咱们再看下面一种静态域的实现:
public class BookStatic { private static BookStatic bookStatic= new BookStatic(); public static BookStatic getBookStatic(){ return bookStatic; } }
JVM在类被加载以后和被线程使用以前,会进行静态初始化,而在这个初始化阶段将会得到一个锁,从而保证在静态初始化阶段内存写入操做将对全部的线程可见。
上面的例子定义了static变量,在静态初始化阶段将会被实例化。这种方式叫作提早初始化。
下面咱们再看一个延迟初始化占位类的模式:
public class BookStaticLazy { private static class BookStaticHolder{ private static BookStaticLazy bookStatic= new BookStaticLazy(); } public static BookStaticLazy getBookStatic(){ return BookStaticHolder.bookStatic; } }
上面的类中,只有在调用getBookStatic方法的时候才会去初始化类。
接下来咱们再介绍一下双重检查加锁。
public class BookDLC { private volatile static BookDLC bookDLC; public static BookDLC getBookDLC(){ if(bookDLC == null ){ synchronized (BookDLC.class){ if(bookDLC ==null){ bookDLC=new BookDLC(); } } } return bookDLC; } }
上面的类中检测了两次bookDLC的值,只有bookDLC为空的时候才进行加锁操做。看起来一切都很完美,可是咱们要注意一点,这里bookDLC必定要是volatile。
由于bookDLC的赋值操做和返回操做并无happens-before,因此可能会出现获取到一个仅部分构造的实例。这也是为何咱们要加上volatile关键词。
本文的最后,咱们将讨论一下在构造函数中含有final域的对象初始化。
对于正确构造的对象,初始化对象保证了全部的线程都可以正确的看到由构造函数为对象给各个final域设置的正确值,包括final域能够到达的任何变量(好比final数组中的元素,final的hashMap等)。
public class FinalSafe { private final HashMap<String,String> hashMap; public FinalSafe(){ hashMap= new HashMap<>(); hashMap.put("key1","value1"); } }
上面的例子中,咱们定义了一个final对象,而且在构造函数中初始化了这个对象。那么这个final对象是将不会跟构造函数以后的其余操做重排序。
本文的例子能够参考https://github.com/ddean2009/learn-java-concurrency/tree/master/reorder
更多内容请访问 flydean的博客