尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【Java并发编程系列1】基础知识(原子性、可见性、有序性,以及内存模型JMM)

【Java并发编程系列1】基础知识(原子性、可见性、有序性,以及内存模型JMM) 并发编程基本概念原子性一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断要么就都不执行。原子性是拒绝多线程操作的不论是多核还是单核具有原子性的量同一时刻只能有一个线程来对它进行操作。简而言之在整个操作过程中不会被线程调度器中断的操作都可认为是原子性。例如 a1是原子性操作但是a和a 1就不是原子性操作。Java中的原子性操作包括基本类型的读取和赋值操作且赋值必须是值赋给变量变量之间的相互赋值不是原子性操作所有引用reference的赋值操作java.concurrent.Atomic.* 包中所有类的一切操作。可见性指当多个线程访问同一个变量时一个线程修改了这个变量的值其他线程能够立即看得到修改的值。在多线程环境下一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性当一个变量被volatile修饰后表示着线程本地内存无效当一个线程修改共享变量后他会立即被更新到主内存中其他线程读取共享变量时会直接从主内存中读取。当然synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。有序性即程序执行的顺序按照代码的先后顺序执行。Java内存模型中的有序性可以总结为如果在本线程内观察所有操作都是有序的如果在一个线程中观察另一个线程所有操作都是无序的。前半句是指“线程内表现为串行语义”后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。在Java内存模型中为了效率是允许编译器和处理器对指令进行重排序当然重排序不会影响单线程的运行结果但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL双重检查锁。另外可以通过synchronized和Lock来保证有序性synchronized和Lock保证每个时刻是有一个线程执行同步代码相当于是让线程顺序执行同步代码自然就保证了有序性。为了让大家更好理解可见性和有序性这个就不得不了解“内存模型”、“重排序”和“内存屏障”因为这三个概念和他们关系非常密切。内存模型JMM决定一个线程对共享变量的写入何时对另一个线程可见JMM定义了线程和主内存之间的抽象关系共享变量存储在主内存(Main Memory)中每个线程都有一个私有的本地内存Local Memory本地内存保存了被该线程使用到的主内存的副本拷贝线程对变量的所有操作都必须在工作内存中进行而不能直接读写主内存中的变量。对于普通的共享变量来讲线程A将其修改为某个值发生在线程A的本地内存中此时还未同步到主内存中去而线程B已经缓存了该变量的旧值所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题可以使用volatile、synchronized、final等此时A、B的通信过程如下首先线程A把本地内存A中更新过的共享变量刷新到主内存中去然后线程B到主内存中去读取线程A之前已更新过的共享变量。JMM通过控制主内存与每个线程的本地内存之间的交互来为java程序员提供内存可见性保证需要注意的是JMM是个抽象的内存模型所以所谓的本地内存主内存都是抽象概念并不一定就真实的对应cpu缓存和物理内存。总结一句话内存模型JMM控制多线程对共享变量的可见性重排序重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则重排序操作不会对存在数据依赖关系的操作进行重排序。比如a1;ba; 这个指令序列由于第二个操作依赖于第一个操作所以在编译时和处理器运行时这两个操作不会被重排序。重排序是为了优化性能但是不管怎么重排序单线程下程序的执行结果不能被改变。比如a1;b2;cab这三个操作第一步a1)和第二步(b2)由于不存在数据依赖关系 所以可能会发生重排序但是cab这个操作是不会被重排序的因为需要保证最终的结果一定是cab3。重排序在单线程下一定能保证结果的正确性但是在多线程环境下可能发生重排序影响结果请看下面的示例代码class ReorderExample { int a 0; boolean flag false; public void writer() { a 1; //1 flag true; //2 } Public void reader() { if (flag) { //3 int i a * a; //4 System.out.println(i); } } }flag变量是个标记用来标识变量a是否已被写入。这里假设有两个线程A和BA首先执行writer()方法随后B线程接着执行reader()方法。线程B在执行操作4时输出是多少呢答案是可能是0也可能是1。由于操作1和操作2没有数据依赖关系编译器和处理器可以对这两个操作重排序同样操作3和操作4没有数据依赖关系编译器和处理器也可以对这两个操作重排序。让我们先来看看当操作1和操作2重排序时可能会产生什么效果请看下面的程序执行时序图如上图所示操作1和操作2做了重排序。程序执行时线程A首先写标记变量flag随后线程B读这个变量。由于条件判断为真线程B将读取变量a。此时变量a还根本没有被线程A写入在这里多线程程序的语义被重排序破坏了最后输出i的结果是0。温馨提示这里其实理解起来有点绕比如线程A先执行了writer()然后线程B执行reader()对于线程A怎么会有这个重排序呢其实这个重排序是对线程B而言的不是线程A哈有了线程B这第一视角我们再理解一下虽然线程A将writer()执行了执行顺序是a1flagtrue但是对于线程B来说因为重排序线程B是根据重排序后的结果去执行的所以才会出现上述异常情况这么给大家解释是不是就清晰很多呢下面再让我们看看当操作3和操作4重排序时会产生什么效果借助这个重排序可以顺便说明控制依赖性。下面是操作3和操作4重排序后程序的执行时序图在程序中操作3和操作4存在控制依赖关系。当代码中存在控制依赖性时会影响指令序列执行的并行度。为此编译器和处理器会采用猜测Speculation执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例执行线程B的处理器可以提前读取并计算a*a此时结果为0然后把计算结果临时保存到一个名为重排序缓冲reorder buffer ROB的硬件缓存中。当接下来操作3的条件判断为真时就把该计算结果写入变量i中。从图中我们可以看出猜测执行实质上对操作3和4做了重排序。重排序在这里破坏了多线程程序的语义因为temp的值为0所以最后输出i的结果是0。那如何避免重排序对多线程的影响呢答案是“内存屏障”内存屏障为了保证内存可见性可以通过volatile、final等修饰变量java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。内存屏障主要有3个功能它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置也不会把前面的指令排到内存屏障的后面即在执行到内存屏障这句指令时在它前面的操作已经全部完成它会强制将对缓存的修改操作立即写入主存如果是写操作它会导致其他CPU中对应的缓存行无效。假如我对上述示例的falg变量通过volatile修饰class ReorderExample { int a 0; boolean volatile flag false; public void writer() { a 1; //1 flag true; //2 } Public void reader() { if (flag) { //3 int i a * a; //4 System.out.println(i); } } }这个时候volatile禁止指令重排序也有一些规则因为篇幅原因该规则将会在下一章讲解根据happens before规则这个过程建立的happens before 关系可以分为两类根据程序次序规则1 happens before 2; 3 happens before 4。根据volatile规则2 happens before 3。根据happens before 的传递性规则1 happens before 4。happens before规则其实就是重排序规则建立的代码前后依赖关系。温馨提示这里大家可能会有疑问1、3的规则我理解但是对于2为什么“2 happens before 3”还记得前面讲的“内存模型”么因为你对变量flag指定了volatile所以当线程A执行完后变量flagtrue会直接刷到内存中然后B马上可见所以说2一定是在3前面不可能因为重排序导致3在2前面执行。然后还要提示一下这里执行时有个前提条件就是线程A执行完才能执行线程B里面的逻辑因为线程A不执行完flag一直是false线程B根本就进不到主流程所以你也可以直接理解为线程A执行完后再执行线程B才有这么个先后关系。上述happens before关系的图形化表现形式如下在上图中每一个箭头链接的两个节点代表了一个happens before 关系。黑色箭头表示程序顺序规则橙色箭头表示volatile规则蓝色箭头表示组合这些规则后提供的happens before保证。这里A线程写一个volatile变量后B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量在B线程读同一个volatile变量后将立即变得对B线程可见。总结今天讲解了Java并发编程的3个特性然后基于里面的两个特性“可见性”和“有序性”引出几个重要的概念分别为“内存模型JMM”、“重排序”和“内存屏障”这个对后续理解volatile、synchronized、final以及避免使用的各种坑真的是非常非常重要所以这块知识要必须一定要掌握。不算之前看的内容光写这篇文章就写了一个下午。这篇文章涉及的知识参考了大量网上的资料我可以说我这篇文章写的比网上绝大部分的文章要好我看了程晓明的《深入理解Java内存模型》里面的内容虽然很好但是很多知识有些啰嗦我只提取了最重要的部分然后也有网上的文章写的很经典但是对于一些概念和示例的阐述深度还不够我结合他们的利弊然后整理了这篇文章详细大家看完这篇文章后再看其它的文章应该就感觉好理解很多。
返回列表