怎么免费建设金融网站,手机网站模板代码,中国网站开发公司排名,动漫设计专业学校并发和并行
并发和并行的目的都是为了使CPU的使用率最大化#xff0c;这两个概念也是我们容易混淆的。
并行#xff08;Parallel#xff09;
并行是指在同一时刻#xff0c;有多条指令在多个处理器上同时执行#xff0c;因为并行要求程序能同时执行多个操作#xff0c…并发和并行
并发和并行的目的都是为了使CPU的使用率最大化这两个概念也是我们容易混淆的。
并行Parallel
并行是指在同一时刻有多条指令在多个处理器上同时执行因为并行要求程序能同时执行多个操作因此只在多处理器系统中存在。无论从微观还是从宏观来看并行的多条指令都是同时执行的。
并发Concurrency
并发是指在同一时刻只能有一条指令执行但会有多个进程执行被快速地轮换执行这就使得在宏观上看好像多个进程在同时执行但在微观上看同一时刻只有一条指令在执行。并发可以在单处理器系统中存在因为并发只是要求程序假装同时执行多个操作实际上只是把时间分为若干段多个进程快速交替地执行。
并发三大特性
并发的三大特性分别是可见性、原子性和有序性我们平时并发编程出现的bug大多数与这三个特性有关。
可见性
可见性是指当一个线程修改了某个共享变量的值其他的线程也能看到这个共享变量修改后的值。在Java内存模型的设计中是通过在共享变量被修改后将修改后的值同步回主内存当其他线程读取变量时先从主内存刷新共享变量的值这种依赖主内存作为传递媒介的方法来实现可见性的。
我们在使用多个线程时如果不做任何处理一个线程修改了共享变量的值其他线程是看不到修改后的值的例如下面的程序
public class VisibilityTest {private boolean flag true;public void refresh(){flag false;System.out.println(Thread.currentThread().getName() 修改flag为false);}public void load(){System.out.println(Thread.currentThread().getName() 开始执行);int count 0;while (flag) {count;}System.out.println(Thread.currentThread().getName() 跳出循环,count count);}public static void main(String[] args) throws InterruptedException {VisibilityTest visibility new VisibilityTest();new Thread(visibility::load,A线程).start();Thread.sleep(1000);new Thread(visibility:: refresh,B线程).start();}
}
执行之后发现程序一直没有结束控制台打印如下图明显A线程没有“看到”B线程修改的flag的值这就是可见性带来的问题。 为了保证共享变量的可见性我们可以采用下面任意一种方式
volatile关键字内存屏障synchronized关键字Lock锁final关键字。
有序性
有序性是指程序执行的顺序按照代码的先后顺序执行。存在有序性问题的原因是JVM在有些情况下为了提高执行效率会对代码进行指令重排等操作这种操作在单线程环境下是没问题的但在多线程环境就会出现意料之外的问题。
为了保证程序的有序性我们可以采用下面任意一种方式
volatile关键字内存屏障synchronized关键字Lock锁。
原子性
原子性是指一个或多个操作要么全部执行且在执行过程中不被任何因素打断要不全部不执行。
为了保证原子性我们可以采用下面任意一种方式
synchronized关键字Lock锁CAS。
Java内存模型Java Memory ModelJMM
Java虚拟机规范中定义了Java内存模型用于屏蔽掉各种硬件和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的并发效果。JMM规范了Java虚拟机与计算机内存是如何协同工作的规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值以及在必须时如何同步地访问共享变量。JMM描述的是一种抽象的概念或者说是一组规则通过这组规则控制程序中各个变量在共享数据区和私有数据区域的访问方式。
JMM规定了所有的共享变量都存储在主内存中每个线程拥有自己的本地内存线程在本地内存中保存了被该线程使用的共享变量的主内存的副本线程对共享变量的所有操作都必须在本地内存中进行不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存的变量线程间变量值的传递都需要通过主内存来完成。线程、本地内存和主内存的关系可以表示如下图 JMM与硬件内存架构
需要注意的是JMM与硬件内存架构没有关系或许JMM的实现参照了硬件内存架构CPU缓存和主存的设计。线程和堆可以出现在硬件架构的主内存、CPU缓存和CPU内存的寄存器中因此如果硬要说JMM与硬件内存架构有关系的话可以表示如下图 内存交互操作
关于主内存与工作内存之间的具体交互协议即一个变量如何从主内存拷贝到工作内存以及如何从工作内存同步到主内存之间的实现细节JMM定义了以下8中操作来完成
lock锁定作用于主内存的变量把一个变量标识为一条线程独占状态unlock解锁作用于主内存变量把一个处于锁定状态的变量释放出来释放后的变量才可以被其他线程锁定read读取作用于主内存变量把一个变量值从主内存传输到线程的工作内存中以便于随后的load动作使用这里可以理解为只是将变量值放到工作内存中但并没有为其赋值load载入作用于工作内存的变量把read操作从主内存中得到的变量值放入工作内存的变量副本中这一步才是真正为工作内存的变量副本赋值use使用作用于工作内存的变量把工作内存中的一个变量值传递给执行引擎每当虚拟机遇到一个需要使用变量的值的字节码指令时都将会执行这个操作assign赋值作用于工作内存的变量把一个从执行引擎接收到的值赋值给工作内存的变量每当虚拟机遇到一个给变量赋值的字节码指令时会执行这个操作store存储作用于工作内存的变量把工作内存中的一个变量的值传送到主内存中以便于随后的write操作write写入作用于主内存的变量把store操作从工作内存中一个变量的值传送到主内存的变量中。
但这些操作并不是随意执行的JMM规定了在执行这8种基本操作时还需满足以下规则
如果要把一个变量从主内存复制到工作内存中必须按顺序地执行read和load操作如果要把一个变量从工作内存同步回主内存中必须按顺序地执行store和write操作。但JMM只规定了上述操作必须顺序执行没有规定必须是连续执行不允许read和load、store和write操作之一单独出现不允许一个线程丢弃它最近的assign赋值的操作即变量在工作内存中改变了之后必须同步到主内存不允许一个线程无原因地没有发生过任何assign操作把数据从工作内存同步回主内存一个新的变量只能在主内存中诞生不允许在工作内存中直接使用一个未被初始化的变量即对一个变量实施use和store操作之前必须先执行过assign和load的操作一个变量在同一时刻只允许一条线程对其进行lock操作但lock操作可以被同一条线程重复执行多次多次执行lock后只有执行相同次数的unlock操作变量才会被解锁即lock和unlock必须成对地出现如果对一个变量执行lock操作将会清空工作内存中此变量的值在执行引擎使用这个变量值前需要重新执行load或assign操作初始化变量的值如果一个变量事先没有被lock操作锁定不允许对它执行unlock操作也不允许去unlock一个被其他线程锁定的变量对一个变量执行unlock操作之前必须先执行store和write操作将变量值同步到主内存。
顺序一致性模型
顺序一致性模型是一个被计算机科学家理想化了的理论参考模型它为程序员提供了极强的内存可见性保证。顺序一致性模型有以下两大特性
一个线程中的所有操作必须按照程序的顺序来执行。即一个线程看到的执行顺序与程序代码的编写顺序是一致的。即便是改变程序代码执行顺序不会影响程序运行结果也必须按照程序代码顺序执行。所有的线程都只能看到一个单一的操作执行顺序。即每个操作都必须原子执行且立刻对所有线程可见多个线程之间不管程序是否同步它们看到的执行顺序都是一致的。
JMM的内存可见性保证
按程序类型Java程序的内存可见性保证可以分为以下三类
单线程程序。单线程程序不会出现内存可见性问题。编译器、runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同正确同步的多线程程序。正确同步的多线程程序的执行具有顺序一致性即执行结果与该程序在顺序一致性模型中的执行结果相同。JMM通过限制编译器和处理器的重排序来提供内存可见性保证未同步或未正确同步的多线程程序。JMM为它们提供了最小安全保障即线程执行时读取到的值要么是之前某个线程写入的值要么是默认值。未同步程序在JMM中执行时整体上是无序的其执行结果无法预知。JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。
顺序一致性模型和JMM内存可见性保证对比
未同步程序在两个模型中的执行特性有如下差异
顺序一致性模型保证单线程内的操作会按程序的顺序执行JMM不保证单线程内的操作会按程序的顺序执行比如正确同步的多线程程序在临界区内的重排序顺序一致性模型保证所有线程只能看到一致的操作执行顺序JMM不保证所有线程能看到一致的操作执行顺序顺序一致性模型保证对所有的内存读/写操作都具有原子性JMM不保证在32位机器上对64位的long型和double型变量的写操作具有原子性。因为JVM在32位处理器上运行时可能会把一个64位long/double变量的写操作拆分为两个32位的写操作来执行这两个32位的写操作可能会被分配到不同的总线事务中执行此时对这个64位变量的写操作将不具有原子性。JDK5开始仅允许把一个64位的long/double型变量的写操作拆分为两个32位的写操作来执行任意的读操作都必须具有原子性。
CPU多级缓存架构
CPU的计算速度是很快的相比之下内存的读取速度就显得很慢了因此CPU如果直接从内存读取数据就需要等待一定的时间周期无疑会降低CPU的效率。为了解决这个问题在CPU和内存中间加入了一种容量很小但速度很高的存储器即高速缓冲存储器CPU缓存。CPU缓存中保存着CPU刚用过或者循环使用的一部分数据当CPU再次使用该部分数据时可从CPU缓存中直接调用这样就减少了CPU的等待时间从而达到提高系统效率的目的。
现代CPU为了提升执行效率减少CPU与内存的交互一般在CPU上集成了多级缓存架构常见的是三级缓存架构可以表示如下图 L1 Cache一级缓存又分为数据缓存和指令缓存分别用来缓存数据和指令是逻辑核独占的。一级缓存是最接近CPU的容量最小但速度最快L2 Cache二级缓存是物理核独占、逻辑核共享的比一级缓存更大一些但速度也更慢一些。二级缓存可以理解为一级缓存的缓冲器一级缓存由于制造成本高导致容量有限二级缓存的作用就是存储那些CPU处理时需要用到但一级缓存又放不下的数据L3 Cache三级缓存是所有物理核共享的比一级缓存和二级缓存的容量都大但速度也是最慢的。三级缓存可以看作是二级缓存的缓冲器这三级缓存容量递增但单位制作成本递减。
当CPU执行运算需要某些数据时首先会去L1寻找找不到再依次去L2、L3和内存中寻找寻找的路径越长耗时也越长。用CPU的时钟CPU自带的时钟时钟周期通常为节拍脉冲或T周期是处理操作的最基本的单位表示从这些区域读取数据的耗时可以表示如下图 上面提到了物理核和逻辑核下面我们简单介绍下相关概念。
物理CPU物理CPU就是插在主机上的真实的CPU硬件在Linux下可以数不同的physical id来确认主机物理CPU的个数核心数多核处理器的核指的就是核心数在Linux下可以通过cores来确认主机的物理CPU的核心数逻辑CPU逻辑CPU与超线程技术有关如果物理CPU不支持超线程技术逻辑CPU的数量与核心数相同如果物理CPU支持超线程那么逻辑CPU的数量是核心数量的两倍在Linux下可以通过processors的数量来确认逻辑CPU的数量物理核可以看得到的真实的CPU核心有独立的电路元件以及L1和L2缓存尅独立地执行命令逻辑核在同一个物理核内逻辑层面的核。物理核通过高速运算让应用程序以为有两个CPU在运算多出来的那个CPU就是逻辑核超线程Hyper-threading超线程可以在一个逻辑核等待指令执行的间隔等待从cache或内存中获取下一条指令把时间片分配给另一个逻辑核高速在这两个逻辑核之间切换让应用程序感知不到这个间隔以为自己独占了一个核。
一个CPU可以有多个物理核如果支持超线程技术一个物理核可以分为n个逻辑核其中n为超线程的数量。
缓存一致性Cache coherence
CPU多级缓存架构解决了内存速度远低于CPU速度造成CPU没有被充分使用的问题但又带来了另一个问题由于每个CPU都有自己的缓存共享资源数据会被存储在这多个本地缓存中当系统中的客户机维护公共内存资源的缓存时可能会出现数据不一致的问题。
例如主内存中有变量x1CPU1和CPU2都有x变量的副本某一时刻CPU1和CPU2都对变量x进行计算CPU1对x1CPU2对x10最终主内存的x变量的值变成了一个不确定的值2或11这明显不是我们想看到的。 为了解决以上问题计算机科学家们提出了缓存一致性的概念。缓存一致性是指存储在多个本地缓存中的共享资源数据的一致性是确保共享数据的变化能够及时地在整个系统中传播的规程。
缓存一致性对共享数据的修改有以下要求
写传播Write Propagation对任何缓存中的数据的更改都必须传播到对等缓存中的其他副本中这里的副本指的是整个缓存行的副本。事务串行化Transaction Serialzation对单个位置的读/写必须被所有处理器以相同的顺序看到。
确保缓存一致性的两种最常见的机制是窥探机制和基于目录的机制。如果有足够的带宽可用基于协议的窥探机制往往更快因为所有事务都是所哟处理器看到的请求/响应。其缺点是窥探是不可扩展的。每个请求都必须光波导系统的所有节点这意味着随着系统的变大总线的带宽及其提供的带宽也必须增加。而基于目录的机制延迟较高但使用更少的带宽因为消息是点对点的而不是广播的因此许多较大的系统64位处理器使用这种类型的缓存一致性。
总线裁决机制
在计算机中数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过一系列步骤来完成的这一系列步骤称之为总线事务Bus Transaction。总线事务包括读事务和写事务读事务从内存传送数据到处理器写事务从处理器传送数据到内存每个事务会读/写内存中一个或多个物理上连续的字总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间会禁止其他的处理器和I/O设备执行内存读/写。
总线可以把所有处理器对内存的访问以串行化的方式来执行。在任意时间点最多只能有一个处理器可以访问内存。这个特性确保了单个总线事务之中的内存读/写操作具有原子性。
原子操作指的是不可被中断的一个或一组操作。处理器会自动保证基本的内存操作的原子性即一个处理器从内存中读取或写入一个字节时其他处理器是不能访问这个字节的内存地址的。最新的处理器可以自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的但处理器对复杂的内存操作是不能自动保证其原子性的例如跨总线宽度、跨多个缓存行和跨页表的访问。处理器提供了总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
总线锁定
总线锁定是使用处理器提供的一个LOCK#信号当其中一个处理器在总线上输出此信号时其他处理器的请求将被阻塞住该处理器可以独占共享内存。
总线锁定阻止了被阻塞处理器和所有内存之间的通信而输出LOCK#信号的CPU可能只需要锁定特定的一块内存区域因此总线锁定的开销较大。
缓存锁定
缓存锁定是指内存区域如果被缓存在处理器的缓存行中并且在Lock操作期间被锁定那么当它执行锁操作协会内存时处理器不会在总线上发出LOCK#信号而是修改内部的内存地址并允许它的缓存一致性机制来保证操作的原子性因为缓存一致性机制会组织同时修改由两个以上处理器缓存的内存区域数据当其他处理器回写已被锁定的缓存行的数据时会使该缓存行无效。
总线窥探Bus Snooping
总线窥探是缓存中的一致性控制器监视或窥探总线事务的一种方案其目标是在分布式共享内存系统中维护缓存一致性。包含一致性控制器的缓存称为snoopy缓存。
当特定数据被多个缓存共享时处理器修改了共享数据的值更改必须传播到其他所有具有该数据副本的缓存中。这种更改传播可以防止系统违反缓存一致性。数据变更的通知可以通过总线窥探来完成所有的窥探者都在监视总线上的每一个事务。如果一个修改共享缓存块的事务出现在总线上所有的窥探者都会检查他们的缓存是否有共享块的相同副本如果有则相应的窥探者执行一个动作以确保缓存一致性。这个动作可以是刷新缓存块或使缓存块失效这里还会涉及到缓存块状态的改变这取决于缓存一致性协议。
窥探协议类型
根据管理写操作的本地副本的方式有两种窥探协议
Write-invalidate
当处理器写入一个共享缓存块时其他缓存中的所有共享副本都会通过总线窥探失效。这种方法确保处理器只能一个数据的一个副本其他缓存中的所有其他副本都无效。这是最常用的窥探协议MSI、MESI、MOSI、MOESI和MESIF协议都属于这种类型。
Write-update
当处理器写入一个共享缓存块时其他缓存的所有共享副本都会通过总线窥探更新。这种方式将写数据广播到总线上的所有缓存中比Write-invalidate协议需要更大的总线流量因此这种方式不常见。Dragon和firefly协议属于这种类型。
MESI一致性协议
一致性协议在多处理器系统中应用于高速缓存一致性为了保持一致性计算机科学家们设计了各种模型和协议。这里我们重点介绍其中一个较常见的MESI协议。
MESI协议是一个基于写失效的缓存一致性协议是支持回写缓存的最常用协议。MESI协议规定缓存行有以下四种不同的状态
已修改ModifiedM表示缓存行是脏dirty的与主存的值不同如果别的处理器要读取主存的这块数据该缓存行必须写回主存随后状态修改为S独占ExclusiveE缓存行只在当前缓存中但是是干净的即缓存数据与主存数据的值相同。当别的缓存读取它时状态变为S当当前缓存写该数据时变为M共享SharedS缓存在存在于两个及以上的缓存中且所有缓存行的数据都是未修改的缓存行可以在任意时刻抛弃无效InvalidI由于某个缓存修改了某个缓存行这个缓存行在其他缓存中就变成无效状态。
用我们刚开始介绍缓存一致性时的例子来使用MESI协议再来看下会发生什么如下图。 最开始时x在CPU1缓存和CPU2的缓存的状态都是S随后CPU1对x进行加1操作CPU2对x进行加10操作但CPU1先将x写回缓存此时CPU1的x所在缓存行状态为MCPU2x所在缓存行状态为ICPU2将x写回缓存时发现缓存行状态是I无效的因此再次从主存读取x的值CPU2读取x的值之前CPU1需要将x的值同步回主存CPU2再次对x进行计算最后得到12。
伪共享
如果多个核的线程在操作同一个缓存行的不同变量数据就会出现频繁的缓存失效的情况即使在代码层面看这两个线程操作的数据之间完全没有关系。这种不合理的资源竞争情况就是伪共享Flase Sharing。
例如core1只对变量x做处理core2只对变量y做处理x和y在同一个缓存行内。由于变量x和变量y在这两个核的L1和L2缓存中都存在当core1对变量x做修改时其他缓存有变量x的缓存行失效就导致core2的变量y的缓存也失效了此时就需要再次从主存读取变量y的值反过来也是一样。
伪共享不会影响程序运行的结果但会降低系统的效率。例如下面的程序中两个线程分别对变量x和变量y自增1亿次最终得到的结果是正确的但耗费的时间较长。需要注意的是x和y必须要是volatile修饰的因为只有volatile修饰的变量才会具有将缓存行设为无效的作用。
public class FalseSharingTest {public static void main(String[] args) throws InterruptedException {testPointer(new Pointer());}private static void testPointer(Pointer pointer) throws InterruptedException {long start System.currentTimeMillis();Thread t1 new Thread(() - {for (int i 0; i 100000000; i) {pointer.x;}});Thread t2 new Thread(() - {for (int i 0; i 100000000; i) {pointer.y;}});t1.start();t2.start();t1.join();t2.join();System.out.println(pointer.x,pointer.y);System.out.println(System.currentTimeMillis() - start);}
}class Pointer {volatile long x;volatile long y;
}
运算结果 为了避免伪共享可以采用以下两种方式。
缓存行填充
我们可以在变量x和变量y之间加入一些变量使得变量x和变量y不在同一个缓存行中就不会出现伪共享问题了。例如对于64个字节大小的缓存行在变量x和变量y的中间加入7个long类型的变量。
class Pointer {volatile long x;long a1;long a2;long a3;long a4;long a5;long a6;long a7;volatile long y;
}
再次运行上面的程序就会发现效率明显提升了很多。 使用sun.misc.Contended注解
Contended注解可以让被修饰的字段与其他字段隔离开放在单独的缓存行中。需要注意的是使用该注解时需要配置JVM参数-XX:-RestricContended。
使用Contended注解再次运行上面的程序可以得到以下结果。 指令重排序
Java语言规范规定JVM线程内部维持顺序化语义即只要程序的最终结果与它顺序化情况的结果相等那么指令的执行顺序可以与代码顺序不一致这个过程就叫指令的重排序。
重排序的意义在于JVM可以根据处理器特性例如CPU多级缓存系统、多核处理器等 适当地对机器指令进行重排序使机器指令能更符合CPU的执行特性最大限度地发挥机器性能。
在编译器与CPU处理器中都能执行指令的重排序操作从源代码到最终执行可能会经过以下重排序 编译器重排序对于程序代码编译器可以在不改变单线程程序语义的情况下对代码语句的顺序进行重新排序指令集并行的重排序对于CPU指令处理器采用了指令集并行技术来将多条指令重叠执行如果不存在数据依赖处理器可以改变机器指令的执行顺序内存系统重排序由于CPU缓存使用缓冲区的方式延迟写入这个过程会造成多个CPU缓存可见性问题这种可见性问题导致结结果对于指令的先后执行显示不一致表面结果看来好像指令的顺序被改变了内存重排序是造成可见性问题的主要原因所在。
as-if-serial
as-if-serial语义的意思是无论怎么重排序单线程程序的执行结果都不能被改变。为了遵守as-if-serial语义编译器和处理器不会对存在数据依赖关系的操作做重排序因为这种重排序会改变执行结果。但如果操作之间不存在数据依赖关系这些操作就可能被编译器和处理器重排序。
happens-before原则
happens-before原则主要用于判断数据是否存在竞争、线程是否安全依靠happens-before原则我们可以解决在并发环境下两个操作之间是否可能存在冲突的所有问题。
happens-before原则的主要定义如下
如果一个操作happens-before另一个操作则第一个操作的结果对第二个操作可见且第一个操作的执行顺序排在第二个操作之前需要注意的是两个操作之间存在happens-before关系并不意味着最终一定会按照happens-before原则定义的执行顺序执行如果这两个操作经过重排序之后的执行结果与按照happens-before原则执行的结果一致那么这种重排序也是被允许的。
happens-before原则包含的具体的规则如下
程序次序原则一段代码在单线程内的执行结果是有序的虽然编译器、虚拟机和处理器会进行指令的重排序但这种重排序不会影响程序的执行结果因此在单线程内程序顺序执行的结果与重排序后执行的结果是一致的锁定规则一个unlock操作优先发生与后面对同一个锁的lock操作volatile变量规则某个线程对volatile变量的写操作优先于另一个线程对这个变量的读操作这一点是volatile对可见性的保证传递规则如果操作Ahappens-before操作B操作Bhappens-before操作C那么操作A同样happens-before操作C线程启动规则某个线程在执行过程中启动了另一个线程那么第一个线程对共享变量的修改在另一个线程开始执行后确保对该线程可见例如下面的程序中main线程对flag的修改对子线程是可见的
public class VisibilityTest {private boolean flag true;public void modifyFlag() {flag false;}public void print() {while (flag) {}}public static void main(String[] args) throws InterruptedException {Test test new Test();new Thread(() - test.print()).start();Thread.sleep(1000);test.modifyFlag();}
}
程序在运行1S后自动终止了如果该规则不成立则不会终止。 线程终结规则某个线程在执行过程中调用另一个线程的join()方法等待其终止那么另一个线程对共享变量的修改在第一个线程等待返回后是可见的。
volatile
根据JMM模型我们可以知道一个线程对共享变量的修改对其他线程是不可见的不满足happens-before规则的情况下例如下面的程序中第二个线程对flag变量的修改对第一个线程是不可见的因此此程序不会停止。
public class Test {private boolean flag true;public void modifyFlag() {flag false;}public void print() {while (flag) {}}public static void main(String[] args) throws InterruptedException {Test test new Test();new Thread(() - test.print()).start();Thread.sleep(1000);new Thread(() - test.modifyFlag()).start();}
} 要使得第二个线程对flag变量的修改对第一个线程可见最简单的方式就是使用volatile关键字修饰flag修改后运行可以发现程序可以被正常终止。 需要注意的是不要在线程中使用System.out.println()来打印一些东西否则即便不使用volatile关键字修饰flag第二个线程对flag的修改对第一个线程依然是可见的例如下面的程序
public class Test {private boolean flag true;public void modifyFlag() {flag false;System.out.println(***************flagflase********************);}public void print() {while (flag) {System.out.println(嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿);}}public static void main(String[] args) throws InterruptedException {Test test new Test();new Thread(() - test.print()).start();Thread.sleep(1000);new Thread(() - test.modifyFlag()).start();}
}
可以看到在第二个线程修改flag的值之后第一个线程就终止了这表示第二个线程对flag的修改对第一个线程是可见的。 出现这种情况的原因是println()方法会使用synchronized上锁清空线程本地内存随后重新从主内存复制共享变量这时第一个线程本地内存中的flag变量已经变成第二个线程修改后的了所以才会出现这种情况。
回到volatile关键字它具有以下特性
可见性某个线程读一个volatile修饰的共享变量时总能读到所有线程对该变量最后的写入值有序性volatile修饰的变量在其读写操作的前后都会被添加多个内存屏障来禁止指令的重排序以保证有序性原子性volatile可以保证其修饰单个变量读写的原子性不能保证一些复合操作的原子性。
当某个线程对一个volatile变量写时JMM会将该线程对应的本地内存中的共享变量刷新回主内存当某个线程对一个volatile读时JMM会将该线程对应的本地内存中的该变量的副本置为无效然后重新从主内存读取共享变量。这也就保证了volatile变量的可见性。