JMM&并发三大特性(1)
前言
JMM属于整个java并发编程中最难的部分也是最重要的部分(java多线程通信模型—共享内存模型)。
学习内容
1. 并发如何学&并发知识体系介绍
理解并发的三大特性,JMM工作内存和主内存关系,知道多线程之间如何通信的,掌握volatile能保证可见性和有序性,CAS就可以了
2. 并发bug的根源可见性
首先说一下并发和并行的区别:
目标都是最大化CPU的使用率
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的.
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
并发三大特性
可见性:
当一个线程修改了共享变量的值,其他线程能够看到修改的值。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。
3. 可见性案例分析
通过B线程控制A线程的执行
public class VisibilityTest1 {
private boolean flag = true;
private int count = 0;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
}
public void load() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
while (flag) {
//TODO 业务逻辑
count++;
}
System.out.println(Thread.currentThread().getName() + "跳出循环: count=" + count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest1 test = new VisibilityTest1();
// 线程threadA模拟数据加载场景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 让threadA执行一会儿
Thread.sleep(1000);
// 线程threadB通过flag控制threadA的执行时间
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
}
程序执行结果如图:
由程序执行结果可知,变量flag不可见的,所以线程A没有跳出循环。
现将flag变量设置为volatile,则A线程能够跳出循环。结果如下:
4. JMM模型极其内存交互操作详解
JMM定义
java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各
种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效
果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。JMM描述的是一种抽象的概念,一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性、有序性、可见性展开的.
根据上面的案例分析多线程的可见性。
线程A一直都是从本地内存中取得值,从而导致没有跳出循环。
5. volatile内存语义分析
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
6. 深入hotshop源码分析可见性的实现
字节码解释器实现
JVM中的字节码解释器(bytecodeInterpreter),用C++实现了JVM指令,其优点是实现相对
简单且容易理解,缺点是执行慢。
bytecodeInterpreter.cpp
7. 从汇编层面分析可见性的实现硬件层面扩展
在linux系统x86中的实现
orderAccess_linux_x86.inline.hpp
inline void OrderAccess::storeload() { fence(); }
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
x86处理器中利用lock实现类似内存屏障的效果。
lock前缀指令的作用
1. 确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执
行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很
大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低
lock前缀指令的执行开销。
2. LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排
序。
3. LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是
将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新
store buffer的操作会导致其他cache中的副本失效
8. 常用的实现可见性的几种手段
public class VisibilityTest {
// storeLoad JVM内存屏障 ----> (汇编层面指令) lock; addl $0,0(%%rsp)
// lock前缀指令不是内存屏障的指令,但是有内存屏障的效果 副本缓存失效
private volatile boolean flag = true;
private int count = 0; // 将count设置为Integer类型 也能够让A线程跳出循环 通过final关键字保证可见性
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
}
public void load() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
while (flag) {
//TODO 业务逻辑
count++;
// UnsafeFactory.getUnsafe().storeFence(); // 可以跳出循环,内存屏障
// Thread.yield(); //能够跳出循环 上下文切换 释放时间片
// System.out.println(count); //也能够跳出循环,本质还是内存屏障
// LockSupport.unpark(Thread.currentThread()); // 也能够跳出循环
// private volatile int count = 0; 对count设置为volatile也能够跳出循环
//shortWait(1000000); //1ms 当时间长的时候,本地内存会重新读取主内存
//shortWait(1000); // 时间短 不会跳出循环
//总结: Java中可见性如何保证? 方式归类有两种:
// 1. jvm层面 storeLoad内存屏障 ===> x86 lock替代了mfence
// 2. 上下文切换 Thread.yield();
// java
// volatile 锁机制
// 当前线程对共享变量的操作会存在读不到,或者不能立即读到另一个线程对此变量的写操作
// lock 硬件层面扩展 JMM为什么选择共享内存模型
}
System.out.println(Thread.currentThread().getName() + "跳出循环: count=" + count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest1 test = new VisibilityTest1();
// 线程threadA模拟数据加载场景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 让threadA执行一会儿
Thread.sleep(1000);
// 线程threadB通过flag控制threadA的执行时间
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >= end);
}
}
总结:
如何保证可见性:
1、通过 volatile 关键字保证可见性
2、通过 内存屏障保证可见性
3、通过 synchronized 关键字保证可见性
4、通过 Lock保证可见性。
5、通过 final 关键字保证可见性