Open dccmmtop opened 2 years ago
这种模型和 jvm 中的堆不同, JMM 是抽象概念,不真实存在。它是一种规范,指定了程序中的各变量的访问方式
JMM 规定所有变量都存放在主内存,主内存是所有线程共享的,但是线程的操作在线程的工作内存中进行,先从主内存读取到线程的工作内存中,然后执行操作,再将工作内存中的值写入主内存中。线程不能直接操作主内存中的数据
工作内存是线程独有的,不同的线程无法访问到对方的工作内存,线程间的通信必须通过主内存传值进行
JMM 描述的是变量在共享区域和私有区域的访问方式,变量的访问在多线程下会有 可见性,原子性,可见性三大问题
因为有工作内存的划分,一个线程操作修改某变量的值,没有同步到主内存前,其他线程是无法读取到该变量最新的值,就导致了变量在另外的线程不可见。 示例:
public class CodeVisibility { private static boolean initFlag = false; // private volatile static boolean initFlag = false; private static int counter = 0; public static void refresh() { System.out.println("refresh data......."); initFlag = true; System.out.println("refresh data success......."); } public static void main(String[] args) { Thread threadA = new Thread(() -> { while (!initFlag) { counter++; } System.out.println("线程:" + Thread.currentThread().getName() + "当前线程嗅探到initFlag的状态的改变, counter: " + counter); }, "threadA"); threadA.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } Thread threadB = new Thread(() -> { refresh(); }, "threadB"); threadB.start(); } }
结果:
可见看到线程A久久不能结束,虽然线程B此时已经修改了 initFlag 的值,但是线程A无法读取到最新值,因为一直没有和主内存同步
这个关键词可以让变量被修改后立刻使其他线程中的副本可见。在上面的示例代码中,把第3行注释,第4行取消注释后再运行: 可以看到线程A可以立即结束
如果不对 initFlag 添加 volatile 标识,线程A就永远无法读取到initFlag的最新值吗?
不一定, 在判断 initFlag 的值时,CPU 先从缓存中取值,只要缓存失效,就会重新在从内存中加载。那么什么时候缓存会失效呢? 对于CPU缓存来说,分为 L1 L2 L3 三级缓存,也就是离CPU最近的那些寄存器,他们的速度依次递减,容量依次递增。而每次CPU缓存的最小单位不是某个变量所占的空间大小,而是固定的字节 ,这样就能减少CPU和内存交互的次数,更好的利用空间局部原理和时间局部性原理。具体细节可以搜索 CPU缓存相关信息
因为CPU一次会让一批缓存失效,有可能 initFlag 的缓存会随着其他值失效而重新从内存加载最新值。如下例子:
public class CodeVisibility { // initFlag 不再用 volatile 修饰 private static boolean initFlag = false; // 这里 counter 类型从 int 修改成 Integer private static Integer counter = 0; public static void refresh() { System.out.println("refresh data......."); initFlag = true; System.out.println("refresh data success......."); } public static void main(String[] args) { Thread threadA = new Thread(() -> { while (!initFlag) { counter++; } // 线程仍然可以很快的结束,因为 counter 会导致 cpu 缓存失效,重新从主内存加载最新数据 System.out.println("线程:" + Thread.currentThread().getName() + "当前线程嗅探到initFlag的状态的改变, counter: " + counter); }, "threadA"); threadA.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } Thread threadB = new Thread(() -> { refresh(); }, "threadB"); threadB.start(); } }
结果: 那么问题又来了,为什么用 int 不会 导致 cpu 缓存失效呢?
个人推测可能使因为 int 比 Integer 所占用的内存更小,CPU缓存放得下,一直没有触发缓存失效。
先看一个例子:
public class VolatileReOrderSample { //定义四个静态变量 private static int x=0,y=0; private static int a=0,b=0; public static void main(String[] args) throws InterruptedException { int i=0; while (true){ i++; x=0;y=0;a=0;b=0; //开两个线程,第一个线程执行a=1;x=b;第二个线程执行b=1;y=a Thread thread1=new Thread(new Runnable() { @Override public void run() { //线程1会比线程2先执行,因此用nanoTime让线程1等待线程2 0.01毫秒 shortWait(10000); a=1; x=b; } }); Thread thread2=new Thread(new Runnable() { @Override public void run() { b=1; y=a; } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); //等两个线程都执行完毕后拼接结果 String result="第"+i+"次执行x="+x+"y="+y; //如果x=0且y=0,则跳出循环 if (x==0&&y==0){ System.out.println(result); break; }else{ System.out.println(result); } } } //等待interval纳秒 private static void shortWait(long interval) { long start=System.nanoTime(); long end; do { end=System.nanoTime(); }while (start+interval>=end); } } 复制代码
按照正常思维,永远不会发生 x=0 y=0的场景,但事实并非如此: 下面是线程A B 可能的正常执行情况
发生指令重排的情况
处理器为了程序的性能可以对程序的执行顺序进行重排,但是,必须满足重排后的执行结果在单线程下结果不能发生改变 这就是 as-if-serial 语义
为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖的操作进行重排,因为会改变执行结果,如果两个操作不存在依赖关系,就有可能会被重排,就入上面的代码,在线程A中a=1;x=b这两各操作没有依赖关系,就有可能会重新排序成x=b;a=1 , 线程B同理。
a=1;x=b
x=b;a=1
这个执行重排的操作在单线程下没有关系,因为没有影响到最终的执行的结果,但是如果是多线程的场景,就像上面的那个例子,就会发生错误
volatile 另一个作用是禁止指令重排,避免多线程下出现乱序执行的情况 重排规则表:
从上面的规则可以看出:
另外还可以使用 synchronize 和 lock 来保证有序性,因为加锁后,每时每刻只有一个线程执行代码,指令重排对单线程没有影响
看下懒汉模式的单例的问题:
// 懒汉模式 + synchronized 同步锁 + double-check public final class Singleton { private static Singleton instance= null;// 不实例化 private Singleton(){}// 构造函数 public static Singleton getInstance(){// 加同步锁,通过该函数向整个系统提供实例 if(null == instance){// 第一次判断,当 instance 为 null 时,则实例化对象,否则直接返回对象 synchronized (Singleton.class){// 同步锁 if(null == instance){// 第二次判断 instance = new Singleton();// 实例化对象 } } } return instance;// 返回已存在的对象 } }
为了在多线程并发场景下单例仍然有效,加了锁以及双重检测,但是就万无一失了吗?
在第一个判断 if(null == instance) 中,会出先变量instance有值,但是内存区域是空的(没有初始化 ),从而导致程序出现问题。造成这个问题的原因在于 instance = new Singleton(),事实上初始化对象操作不是原子性的,它包含下面两个动作:
if(null == instance)
instance = new Singleton()
其中2,3没有依赖关系,经过 编译器或者cpu指令重排后,可能会导致 2,3顺序发生变化:
假设线程A按照第二种顺序执行,在执行完步骤3时,还没有执行步骤2,线程B执行到第一个if(null == instance)判断,就会直接返回 instance。 这样对于线程B来说 getInstance() 方法返回的是一个没有经过初始化的对象,导致程序出现问题
getInstance()
解决问题的方法很简单: private volatile static Singleton instance= null; 使用 volatile 关键词禁止 instance 变量被执行指令重排优化即可
private volatile static Singleton instance= null;
指的使一个操作是不可中断的,即使在多线程环境下,一旦操作开始就不会被其他线程影响
java 中可以通过 synchronize 和 lock 保证原子性,它们能保证任意时刻只有一个线程访问代码
java内存模型 (JMM)
这种模型和 jvm 中的堆不同, JMM 是抽象概念,不真实存在。它是一种规范,指定了程序中的各变量的访问方式
主内存
JMM 规定所有变量都存放在主内存,主内存是所有线程共享的,但是线程的操作在线程的工作内存中进行,先从主内存读取到线程的工作内存中,然后执行操作,再将工作内存中的值写入主内存中。线程不能直接操作主内存中的数据
工作内存
工作内存是线程独有的,不同的线程无法访问到对方的工作内存,线程间的通信必须通过主内存传值进行
JMM 描述的是变量在共享区域和私有区域的访问方式,变量的访问在多线程下会有 可见性,原子性,可见性三大问题
可见性问题
因为有工作内存的划分,一个线程操作修改某变量的值,没有同步到主内存前,其他线程是无法读取到该变量最新的值,就导致了变量在另外的线程不可见。 示例:
结果:
可见看到线程A久久不能结束,虽然线程B此时已经修改了 initFlag 的值,但是线程A无法读取到最新值,因为一直没有和主内存同步
volatile
这个关键词可以让变量被修改后立刻使其他线程中的副本可见。在上面的示例代码中,把第3行注释,第4行取消注释后再运行: 可以看到线程A可以立即结束
volatile 原理
特性
volatile 番外
如果不对 initFlag 添加 volatile 标识,线程A就永远无法读取到initFlag的最新值吗?
不一定, 在判断 initFlag 的值时,CPU 先从缓存中取值,只要缓存失效,就会重新在从内存中加载。那么什么时候缓存会失效呢? 对于CPU缓存来说,分为 L1 L2 L3 三级缓存,也就是离CPU最近的那些寄存器,他们的速度依次递减,容量依次递增。而每次CPU缓存的最小单位不是某个变量所占的空间大小,而是固定的字节 ,这样就能减少CPU和内存交互的次数,更好的利用空间局部原理和时间局部性原理。具体细节可以搜索 CPU缓存相关信息
因为CPU一次会让一批缓存失效,有可能 initFlag 的缓存会随着其他值失效而重新从内存加载最新值。如下例子:
结果: 那么问题又来了,为什么用 int 不会 导致 cpu 缓存失效呢?
个人推测可能使因为 int 比 Integer 所占用的内存更小,CPU缓存放得下,一直没有触发缓存失效。
有序性问题
先看一个例子:
按照正常思维,永远不会发生 x=0 y=0的场景,但事实并非如此: 下面是线程A B 可能的正常执行情况
发生指令重排的情况
指令重排
处理器为了程序的性能可以对程序的执行顺序进行重排,但是,必须满足重排后的执行结果在单线程下结果不能发生改变 这就是 as-if-serial 语义
为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖的操作进行重排,因为会改变执行结果,如果两个操作不存在依赖关系,就有可能会被重排,就入上面的代码,在线程A中
a=1;x=b
这两各操作没有依赖关系,就有可能会重新排序成x=b;a=1
, 线程B同理。这个执行重排的操作在单线程下没有关系,因为没有影响到最终的执行的结果,但是如果是多线程的场景,就像上面的那个例子,就会发生错误
如何禁止指令重排
volatile
volatile 另一个作用是禁止指令重排,避免多线程下出现乱序执行的情况 重排规则表:
从上面的规则可以看出:
加锁保证有序性
另外还可以使用 synchronize 和 lock 来保证有序性,因为加锁后,每时每刻只有一个线程执行代码,指令重排对单线程没有影响
禁止指令重排的经典应用
看下懒汉模式的单例的问题:
为了在多线程并发场景下单例仍然有效,加了锁以及双重检测,但是就万无一失了吗?
在第一个判断
if(null == instance)
中,会出先变量instance有值,但是内存区域是空的(没有初始化 ),从而导致程序出现问题。造成这个问题的原因在于instance = new Singleton()
,事实上初始化对象操作不是原子性的,它包含下面两个动作:其中2,3没有依赖关系,经过 编译器或者cpu指令重排后,可能会导致 2,3顺序发生变化:
假设线程A按照第二种顺序执行,在执行完步骤3时,还没有执行步骤2,线程B执行到第一个
if(null == instance)
判断,就会直接返回 instance。 这样对于线程B来说getInstance()
方法返回的是一个没有经过初始化的对象,导致程序出现问题解决问题的方法很简单:
private volatile static Singleton instance= null;
使用 volatile 关键词禁止 instance 变量被执行指令重排优化即可原子性问题
指的使一个操作是不可中断的,即使在多线程环境下,一旦操作开始就不会被其他线程影响
java 中可以通过 synchronize 和 lock 保证原子性,它们能保证任意时刻只有一个线程访问代码