Java 中的多线程之内存可见性

来源:互联网 发布:印象笔记 圈点 mac 编辑:程序博客网 时间:2024/05/21 01:46

  • Java内存模型JMM
  • Java 内存模型的两条规定
  • 实现可见性的要求
  • Java 语言层面支持的可见性实现方式
    • 1 synchronize 实现可见性的过程
    • 2 volatile 实现可见性
    • 3 volatile 和 synchronized 比较
  • 导致共享变量在线程间不可见的原因

此文我在转载下文的同时并作了一些补充,讲解的内容是没变的。
http://febsky.me/2016/02/28/2016-02-28-Java%E4%B8%AD%E7%9A%84%E5%A4%9A%E7%BA%BF%E7%A8%8B%E4%B9%8B%E5%86%85%E5%AD%98%E5%8F%AF%E8%A7%81%E6%80%A7/#4-3、Volatile和synchronized比较

1. Java内存模型(JMM)

Java 内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在 JVM 中将变量存储到内存和从内存中读取出变量的底层细节。

  • 所有的变量都存储在主内存中
  • 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量副本(主内存中该变量的一份拷贝)
  • 通常称被多个线程访问的变量为共享变量。

这里写图片描述

2. Java 内存模型的两条规定

线程1对共享变量的修改要想被线程2及时看到,必须经过下列两个步骤:

  • 把工作内存1中更新过的共享变量刷新到主内存中
  • 将主内存中最新的共享变量的值更新到工作内存2中

3. 实现可见性的要求

  • 线程修改后的共享变量值能够及时从工作内存刷新到主内存中
  • 其他线程能够及时把共享变量的最新值从主内存更新到自己的工作内存中

4. Java 语言层面支持的可见性实现方式

  1. synchronized 实现
  2. volatile 关键字

4.1 synchronize 实现可见性的过程

  1. 获取互斥锁
  2. 清空工作内存
  3. 从主内存拷贝变量的最新副本到工作内存
  4. 执行代码
  5. 将更改后的共享变量的值刷新到主内存
  6. 释放互斥锁

4.2 volatile 实现可见性

深入来说:通过加入内存屏障和禁止重排序优化来实现的,强制缓存区缓存失效。
对 volatile 变量执行写操作时,会在写操作后加入一条 store 屏障指令
对 volatile 变量执行读操作的时候,会在读操作前加入一条 load 屏障指令

通俗来说:volatile 变量在每次被线程访问的时候,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值。

线程写 volatile 变量的过程:
1. 改变线程工作内存中 volatile 变量副本的值
2. 将改变后的副本的值从工作内存刷新到主内存

线程读 volatile 变量的过程:
1. 从主内存中读取 volatile 变量的最新值到线程的工作内存中
2. 从工作内存中读取 volatile 变量的副本

volatile 不能保证变量符合操作的原子性。比如 number ++这货就不是原子操作,这货其实是三步操作的。
1. 从内存中取出 number 的值。
2. 计算 number 的值。
3. 将 number 的值写到内存中。
假如在第二步计算值的时候,另外一个线程也修改了 number 的值,那么这个时候就会出现脏数据。

那么如何保证它的原子性,有以下三种方式:

  • 使用 synchronized 关键字
  • 使用 ReentrantLock ( java.util.concurrent.locks 包下面)
  • 使用 AtomicInterger ( java.util.concurrent.atomic 包下面)

演示:

Lock lock = new ReentrantLock();lock.lock();try{    this.number ++;}finally{    lock.unlock();}

4.3 volatile 和 synchronized 比较

  1. volatile 不需要加锁,比 synchronized 轻,不会阻塞线程,所以效率比较高,但稳定性差,比如上面说的不能保证原子性。
    总而言之:synchronized 稳定性高,效率低;volatile 稳定性低,效率高。
  2. 对于可见性:
    当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
    而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
    另外,通过 synchronized 和 Lock 也能够保证可见性,synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
  3. volatile 解决的是变量在多个线程之间的可见性;而 synchronized 解决的是多个线程之间访问资源的同步性。

5. 导致共享变量在线程间不可见的原因

  • 线程的交叉执行
  • 重排序结合线程的交叉执行
  • 共享变量更新后的值没有在工作内存与主内存间及时更新