可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步当我们声明共享变量为volatile后,对这个变量的读/写将会很特别。理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。
假设有多个线程分别调用上面程序的三个方法,这个程序在语义上和下面程序等价:
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:
指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
再想象一下,只有线程1对counter变量进行增加操作,但线程1和线程2都可能读取变量counter。如果counter变量未声明volatile,则无法保证何时将counter变量的值从CPU缓存写回主存储器。这意味着,CPU高速缓存中的counter变量值可能与主存储器中的变量值不同。这种情况如下所示:
Java volatile关键字旨在解决变量可见性问题。通过使用volatile声明counter变量,对变量counter的所有写操作都将立即写回主存储器。此外,counter变量的所有读取都将直接从主存储器中读取。下面是counter变量声明为volatile的样子:
声明变量为volatile,对其他线程写入该变量 保证了可见性。在上面给出的场景中,一个线程(T1)修改计数器,另一个线程(T2)读取计数器(但从不修改它),声明该counter变量为volatile足以保证写入counter变量对T2的可见性。
实际上,Java volatile的可见性保证超出了volatile变量本身。可见性保证如下:
udpate()方法写入三个变量,其中只有days是volatile变量。完全volatile可见性保证意味着,当将一个值写入days时,对线程可见的其他所有变量也会写入主存储器。这意味着,当一个值被写入days,years和months的值也被写入主存储器(注意days的写入在最后)。
当读取years,months和days的值你可以这样做:
即程序执行的顺序按照代码的先后顺序执行。java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:
出于性能原因允许JVM和CPU重新排序程序中的指令,只要指令的语义含义保持不变即可。例如,查看下面的指令:
这些指令可以按以下顺序重新排序,而不会丢失程序的语义含义:
上面讲的是volatile变量自身的特性,对程序员来说,volatile对线程的内存可见性的影响比volatile自身的特性更为重要,也更需要我们去关注。从JSR-133开始,volatile变量的写-读可以实现线程之间的通信。
请看下面使用volatile变量的示例代码:
上述happens before 关系的图形化表现形式如下:
为了解决指令重排序挑战,除了可见性保证之外,Java volatile关键字还提供“happens-before”保证。happens-before保证保证:volatile 之前读写
即使volatile关键字保证volatile变量的所有读取直接从主存储器读取,并且所有对volatile变量的写入都直接写入主存储器,仍然存在声明volatile变量线程不安全。在前面解释的情况中,只有线程1写入共享counter变量,声明counter变量为volatile足以确保线程2始终看到最新的写入值。
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。以上面示例程序VolatileExample为例,假设线程A首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。下图是线程A执行volatile写后,共享变量的状态示意图:
当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。下面是线程 B 读同一个 volatile 变量后,共享变量的状态示意图:
下面对volatile写和volatile读的内存语义做个总结
前文我们提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。下面是JMM针对编译器制定的volatile重排序规则表:是否能重排序第二个操作第二个操作第二个操作第一个操作普通读/写volatile读volatile写普通读/写NOvolatile读NONONOvolatile写NONO举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
从上表我们可以看出
下面是在保守策略下,volatile读插入内存屏障后生产的指令序列示意图:
下面我们通过具体的示例代码来说明
针对 readAndWrite() 方法,编译器在生成字节码时可以做如下的优化:
前面保守策略下的volatile读和写,在 x86处理器平台可以优化成:
在 JSR-133 之前的旧 Java 内存模型中,虽然不允许 volatile 变量之间重排序,但旧的 Java 内存模型允许 volatile 变量与普通变量之间重排序。在旧的内存模型中,VolatileExample 示例程序可能被重排序成下列时序来执行:
本文由传智教育博学谷狂野架构师教研团队发布。
如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。
转载请注明出处!
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) | Powered by Discuz! X3.4 |