Java内存模型(JMM)

时间:2022-03-11 17:04:22

Java内存模型

  1. 概念
    内存模型(memory model):在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。
  2. 作用
    java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。(注:此处变量与java编程中所说的变量有所区别,它包括了实例字段、静态字段、构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的,不会被共享)
  3. 主内存和工作内存
    java内存模型规定了所有的变量都存储在主内存中,每条线程拥有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本的拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
  4. 区别
    上面所说的主内存/工作内存与java内存区域中java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本没有关系,如果一定要勉强对应,主内存主要对应于java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。
  5. 主内存和工作内存之间的操作
    虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的
    lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
    unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
    read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
    load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
    use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
    assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
    store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
    write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存变量中。
    注意:

    read/load和write/store
    不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。即java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行,也就是说,read和load之间、store和write之间是可插入其他指令的。
    assign
    不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变之后必须把该变化同步回主内存。
    不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
    lock/unlock


一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)

volatile 变量

  1. volatile
    关键字volatile可以说是java虚拟机提供的最轻量级的同步机制。
  2. volatile变量的两种特性
    第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说可以立即得知。由于Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。
    第二使用volatile变量的第二个语义是禁止指令重排序优化。
//此处显示了volatile并不能保证线程的安全性
public class Volatile12_1 {

public static volatile int race = 0;
public static void increase()
{
race++;
}
private static final int THREAD_COUNT = 20;
public static void main(String[] args)
{
Thread[] threads = new Thread[THREAD_COUNT];
for(int i = 0;i < THREAD_COUNT;i++)
{
threads[i] = new Thread(new Runnable(){

@Override
public void run() {
for(int i = 0;i < 10000;i++)
increase();
}

});
threads[i].start();
}
while(Thread.activeCount() > 1)
{
Thread.yield();
}
System.out.println(race);

}
}
  1. 在不符合以下两种情况下,需要加锁来保证原子性
    由于volatile变量只能保证可见性,不符合以下两条规则的运算场景中,需要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。
    第一运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
    第二变量不需要与其他的状态变量共同参与不变约束。
  2. 总结
    对于volatile型变量的特殊规则,线程对变量的use动作可以认为是和线程对变量的load、read动作相关联,必须连续一起出现,即每次使用volatile变量的时候,都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量所做的修改后的值。
    线程对变量的assign动作可以认为是和线程对变量的store、write动作关联,必须连续一起出现(即要求在工作内存中,每次修改变量后必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量所做的修改)
    1. 普通变量和volatile变量之间的区别
      volatile的特殊规保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。