网站php怎么做的,免费做销售网站有哪些,室内效果图制作流程,老域名怎么做新网站文章目录 1. Java内存模型2. 内存交互3. 三大特性3.1 可见性3.1.1 可见性问题3.1.2 原因3.1.3 解决方法 3.2 原子性3.3 有序性 4. 指令重排5. JMM 与 happens-before5.1 happens-before关系定义5.2 happens-before 关系 在继续学习JUC之前#xff0c;我们现在这里介绍一下Java… 文章目录 1. Java内存模型2. 内存交互3. 三大特性3.1 可见性3.1.1 可见性问题3.1.2 原因3.1.3 解决方法 3.2 原子性3.3 有序性 4. 指令重排5. JMM 与 happens-before5.1 happens-before关系定义5.2 happens-before 关系 在继续学习JUC之前我们现在这里介绍一下Java内存模型也就是JMM进而引出关键字volatile的使用条件。
1. Java内存模型
Java 内存模型是 Java Memory ModelJMM本身是一种抽象的概念实际上并不存在描述的是一组规则或规范通过这组规范定义了程序中各个变量包括实例字段静态字段和构成数组对象的元素的访问方式
JMM 作用
屏蔽各种硬件和操作系统的内存访问差异实现让 Java 程序在各种平台下都能达到一致的内存访问效果规定了线程和内存之间的一些关系
根据 JMM 的设计系统存在一个主内存Main MemoryJava 中所有变量都存储在主存中对于所有线程都是共享的每条线程都有自己的工作内存Working Memory工作内存中保存的是主存中某些变量的拷贝线程对所有变量的操作都是先对变量进行拷贝然后在工作内存中进行不能直接操作主内存中的变量线程之间无法相互直接访问线程间的通信传递必须通过主内存来完成 主内存和工作内存
主内存计算机的内存也就是经常提到的 8G 内存16G 内存存储所有共享变量的值工作内存存储该线程使用到的共享变量在主内存的的值的副本拷贝
JVM 和 JMM 之间的关系JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分这两者基本上是没有关系的如果两者一定要勉强对应起来
主内存主要对应于 Java 堆中的对象实例数据部分而工作内存则对应于虚拟机栈中的部分区域从更低层次上说主内存直接对应于物理硬件的内存工作内存对应寄存器和高速缓存
2. 内存交互
Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作每个操作都是原子的
非原子协定没有被 volatile 修饰的 long、double 外默认按照两次 32 位的操作 lock作用于主内存将一个变量标识为被一个线程独占状态对应 monitorenterunclock作用于主内存将一个变量从独占状态释放出来释放后的变量才可以被其他线程锁定对应 monitorexitread作用于主内存把一个变量的值从主内存传输到工作内存中load作用于工作内存在 read 之后执行把 read 得到的值放入工作内存的变量副本中use作用于工作内存把工作内存中一个变量的值传递给执行引擎每当遇到一个使用到变量的操作时都要使用该指令assign作用于工作内存把从执行引擎接收到的一个值赋给工作内存的变量store作用于工作内存把工作内存的一个变量的值传送到主内存中write作用于主内存在 store 之后执行把 store 得到的值放入主内存的变量中
3. 三大特性
3.1 可见性
可见性是指当多个线程访问同一个变量时一个线程修改了这个变量的值其他线程能够立即看得到修改的值
3.1.1 可见性问题
存在不可见问题的根本原因是由于缓存的存在线程持有的是共享变量的副本无法感知其他线程对于共享变量的更改导致读取的值不是最新的。但是 final 修饰的变量是不可变的就算有缓存也不会存在不可见的问题。
如下面的代码所示
Slf4j(topic c.Test20)
public class Test1 {static boolean run true; //添加volatilepublic static void main(String[] args) throws InterruptedException {Thread t new Thread(()-{while(run){// ....}});t.start();sleep(1);log.debug(我想停下t);run false; // 线程t不会如预想的停下来}
}可以看到上面的代码在主线程的最后将 run 变量设置为了 false 但是线程t并没有像我们预期的一样停下来这就是由于缓存存在的原因。
3.1.2 原因
我们可以分析下上面的流程 初始状态 t 线程刚开始从主内存读取了 run 的值到工作内存 因为 t 线程要频繁从主内存中读取 run 的值JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中减少对主存中 run 的访问提高效率。 1 秒之后main 线程修改了 run 的值并同步至主存而 t 是从自己工作内存中的高速缓存中读取这个变量的值结果永远是旧值。
3.1.3 解决方法 使用volatile 它可以用来修饰成员变量和静态成员变量他可以避免线程从自己的工作缓存中查找变量的值必须到主存中获取它的值线程操作 volatile 变量都是直接操作主存修改如下 Slf4j(topic c.Test20)
public class Test1 {volatile static boolean run true; //添加volatilepublic static void main(String[] args) throws InterruptedException {Thread t new Thread(()-{while(run){// ....}});t.start();sleep(1);log.debug(我想停下t);run false; // 线程t不会如预想的停下来}
}使用synchronized锁 Slf4j(topic c.Test20)
public class Test1 {volatile static boolean run true; //添加volatilefinal static Object lock new Object();public static void main(String[] args) throws InterruptedException {Thread t new Thread(()-{while(run){synchronized (lock){if(!run){break;}}}});t.start();sleep(1);log.debug(我想停下t);synchronized (lock){run false;}}
}3.2 原子性
原子性不可分割完整性也就是说某个线程正在做某个具体业务时中间不可以被分割需要具体完成要么同时成功要么同时失败保证指令不会受到线程上下文切换的影响
定义原子操作的使用规则
不允许 read 和 load、store 和 write 操作之一单独出现必须顺序执行但是不要求连续不允许一个线程丢弃 assign 操作必须同步回主存不允许一个线程无原因地没有发生过任何 assign 操作把数据从工作内存同步会主内存中一个新的变量只能在主内存中诞生不允许在工作内存中直接使用一个未被初始化assign 或者 load的变量即对一个变量实施 use 和 store 操作之前必须先自行 assign 和 load 操作一个变量在同一时刻只允许一条线程对其进行 lock 操作但 lock 操作可以被同一线程重复执行多次多次执行 lock 后只有执行相同次数的 unlock 操作变量才会被解锁lock 和 unlock 必须成对出现如果对一个变量执行 lock 操作将会清空工作内存中此变量的值在执行引擎使用这个变量之前需要重新从主存加载如果一个变量事先没有被 lock 操作锁定则不允许执行 unlock 操作也不允许去 unlock 一个被其他线程锁定的变量对一个变量执行 unlock 操作之前必须先把此变量同步到主内存中执行 store 和 write 操作
3.3 有序性
JAVA 运行时有的操作是无序的无序是因为发生了指令重排序。
CPU 的基本工作是执行存储的指令序列即程序程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程为了提高性能编译器和处理器会对指令重排一般分为以下三种
源代码 - 编译器优化的重排 - 指令并行的重排 - 内存系统的重排 - 最终执行指令比如下面的代码
int num 0;
boolean ready false;
// 线程1 执行此方法
public void actor1(I_Result r) {if(ready) {r.r1 num num;} else {r.r1 1;}
}
// 线程2 执行此方法
public void actor2(I_Result r) {num 2;ready true;
}如果执行上面的两个线程那么可能的结果有哪些呢很明显有下面两种
先执行线程1再执行线程2那么结果是1先执行线程2再执行线程1那么结果是4但是除此之外还有一种发生重排的情况即线程2的指令重排为先执行 ready true; 再执行 num 2; 这样的话结果就是0
如果我们希望指令不进行重排那么可以再定义变量时加一个volatile修饰变量 ready 这样可以使得ready代码之前的部分不发生重排所以 num 就不用加 volatile修饰了。
4. 指令重排
上面我们提到了有时候指令并不按照我们编写代码的顺序来进行执行这是因为发生了指令重排那么为什么要发生指令重排呢不发生指令重排程序不是更加显明吗
其实不是这样的每一个指令都会包含多个步骤每个步骤可能使用不同的硬件。因此流水线技术产生了它的原理是指令 1 还没有执行完就可以开始执行指令 2而不用等到指令 1 执行结束后再执行指令 2这样就大大提高了效率指令流水线如下图所示
上述流水线技术能够很好的提高CPU的效率但是流水线术最害怕中断恢复中断的代价是比较大的所以我们要想尽办法不让流水线中断。指令重排就是减少中断的一种技术。 我们分析一下下面这段代码的执行情况
a b c;
d e - f ;先加载 b、c注意有可能先加载 b也有可能先加载 c但是在执行 add(b,c) 的时候需要等待 b、c 装载结束才能继续执行也就是需要增加停顿那么后面的指令加载 e 和 f也会有停顿这就降低了计算机的执行效率。
为了减少停顿我们可以在加载完 b 和 c 后把 e 和 f 也加载了然后再去执行 add(b,c) 这样做对程序串行是没有影响的但却减少了停顿。换句话说既然 add(b,c) 需要停顿那还不如去做一些有意义的事情加载 e 和 f。
因此指令重排对于提高 CPU 性能十分必要但也带来了乱序的问题。
指令重排一般分为以下三种
编译器优化重排编译器在不改变单线程程序语义的前提下重新安排语句的执行顺序。指令并行重排现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果)处理器可以改变语句对应的机器指令的执行顺序。内存系统重排由于处理器使用缓存和读写缓存冲区这使得加载(load)和存储(store)操作看上去可能是在乱序执行因为三级缓存的存在导致内存与缓存的数据同步存在时间差。
指令重排可以保证串行语义一致但是没有义务保证多线程间的语义也一致。所以在多线程下指令重排序可能会导致一些问题。
5. JMM 与 happens-before
5.1 happens-before关系定义
一方面我们开发者需要 JMM 提供一个强大的内存模型来编写代码另一方面编译器和处理器希望 JMM 对它们的束缚越少越好这样它们就可以尽可能多的做优化来提高性能希望的是一个弱的内存模型。
JMM 考虑了这两种需求并且找到了平衡点对编译器和处理器来说只要不改变程序的执行结果单线程程序和正确同步了的多线程程序编译器和处理器怎么优化都行。
对于我们开发者来说JMM 提供了happens-before 规则JSR-133 规范满足了我们的诉求——简单易懂并且提供了足够强的内存可⻅性保证。 换言之我们开发者只要遵循 happens-before 规则那么我们写的程序就能保证在 JMM 中具有强的内存可⻅性。
JMM 使用 happens-before 的概念来定制两个操作之间的执行顺序。这两个操作可以在一个线程内也可以是不同的线程种。
happens-before 关系的定义如下
如果一个操作 happens-before 另一个操作那么第一个操作的执行结果将对第二个操作可⻅而且第一个操作的执行顺序排在第二个操作之前。两个操作之间存在 happens-before 关系并不意味着 Java 平台的具体实现必须要按照 happens-before关系指定的顺序来执行。如果重排序之后的执行结果与按 happens-before 关系来执行的结果一致那么JMM 也允许这样的重排序。
总之如果操作 A happens-before 操作 B那么操作 A 在内存上所做的操作对操作 B 都是可⻅的不管它们在不在一个线程。
5.2 happens-before 关系
在Java中有以下天然的happens-before 关系 程序次序规则 (Program Order Rule)一个线程内逻辑上书写在前面的操作先行发生于书写在后面的操作 因为多个操作之间有先后依赖关系则不允许对这些操作进行重排序 锁定规则 (Monitor Lock Rule)一个 unlock 操作先行发生于后面时间的先后对同一个锁的 lock 操作所以线程解锁 m 之前对变量的写解锁前会刷新到主内存中对于接下来对 m 加锁的其它线程对该变量的读可见 volatile 变量规则 (Volatile Variable Rule)对 volatile 变量的写操作先行发生于后面对这个变量的读 传递规则 (Transitivity)具有传递性如果操作 A 先行发生于操作 B而操作 B 又先行发生于操作 C则可以得出操作 A 先行发生于操作 C 线程启动规则 (Thread Start Rule)Thread 对象的 start()方 法先行发生于此线程中的每一个操作 static int x 10;//线程 start 前对变量的写对该线程开始后对该变量的读可见
new Thread(()-{ System.out.println(x); },t1).start();线程中断规则 (Thread Interruption Rule)对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生 线程终止规则 (Thread Termination Rule)线程中所有的操作都先行发生于线程的终止检测可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行 对象终结规则Finaizer Rule一个对象的初始化完成构造函数执行结束先行发生于它的 finalize() 方法的开始