对于volatile我们知道它用来修饰变量的,同时可以保证变量的可见性以及禁止重排序。
可见性的理解是对于一个被volatile修饰的变量,当一个线程修改该变量时,对其它线程立即可见,保证其它线程获取的该变量是最新的。
对于禁止重排序来讲,程序在执行时会经过编译器优化重排序、指令级重排序、内存系统重排序,经过这些重排序可以最大化的执行我们的程序,而volatile所产生的禁止重排序就是当对volatile修饰的变量进行读或者写操作时,不会被这些操作重排序,保证顺序执行,得到正确的结果。
而volatile这两个规则的内存语义是怎样的呢?
要了解这些首先要知道主内存以及本地内存的概念,主内存中存储的就是我们定义的变量,本地内存则是依托线程而存在的,每个线程都有自己的本地内存,本地内存存放的是该线程使用到的变量的副本,可以理解为线程对变量做操作时并不是操作的主内存,而是将变量拷贝到自己的本地内存,做完对应的操作后再将变量刷回到主内存中,这样提高了系统的性能。
这里也要了解总线这个概念,因为线程与主内存的交互就是通过总线来实现的,总线控制着哪个线程可以访问主内存,相当于线程的管理员。线程将数据刷回到主内存的操作也是通过总线来完成的。
1、可见性
volatile的可见性而言,需要从volatile读和volatile写操作理解,当线程A进行volatile写操作时,线程B执行volatile读操作,如果没有可见性,那么线程B拿到的可能是脏数据,而volatile的实现原理是当一个线程执行读操作时,JMM会将该线程中的本地缓存设置为无效,这样该线程从知道本地内存中的值已经失效了,那么就会去主内存中获取数据。
对于volatile读和volatile写的内存语义而言它更像是线程之间的通信,也就是:
线程A执行volatile写操作,那么它会发送消息给其它需要读这个变量的线程,
当其它线程执行volatile读操作时,其实就是收到volatile写线程的消息(本地内存中数据失效的消息),这样线程之间实现了通信,所以对于可见性的内存语言而言其实就是线程之间的通信机制。
2、禁止重排序
在说禁止重排序时需要知道内存屏障的概念,内存屏障是一组处理器指令,用来控制对内存操作的顺序性。
内存屏障的类型有四种,分别为;loadload、loadstore、storestore、storeload
loadload:操作1------->loadload-------->操作2,通过loadload屏障保证操作1的数据装载先于操作2之后所有的操作。
storestore:操作1------>storestore-------->操作2,通过storestore屏障保证操作1刷回到主内存的指令先于操作2以及后续所有的操作。
loadstore:操作1------>loadstore-------->操作2,通过loadstore屏障保证操作1的获取数据指令先于操作2以及之后所有刷新到主内存的指令。
storeload:操作1-------->storeload------->操作2,通过storeload屏障保证操作1刷新到主内存的指令先于操作2的获取数据指令。
通过以上的四个内存屏障保证了对数据的读写操作能够按照顺序执行,保证数据的准确性。
说完了内存屏障,那么就分析禁止重排序的内存语义。对于被volatile修饰的变量,当对volatile修饰的数据执行写操作时,会在前面加一个storestore屏障,这样保证前面所有对该变量的操作都已经刷回到了主内存中,那么对该变量操作时就是操作的最新数据。
当对volatile修饰的数据执行写操作时,也会在后面加一个storeload屏障,这样保证了对volatile变量的写操作只有刷回到主内存后,后续的操作才可以拿到该变量。
当对volatile修饰的数据执行读操作时,会在后面加一个loadload屏障,这样就保证了volatile读操作一定比后续的操作要先执行。
当对volatile修饰的数据执行读操作时,会在后面加一个loadstore屏障,这样保证了后续的写操作不会影响该次的读操作,读到的数据是预期的值。
从以上可以知道对于禁止重排序而言其实就是对读写操作执行的顺序性的保证。