1、java内存模型jmm

java内存模型定义的java线程使用内存的规则,主要目标是定义程序变量的访问规则,即虚拟机中变量存储到内存以及从内存访问的细节。这里讲的变量是指线程共享的变量(如实例字段,静态字段,对象),不包含线程私有变量(如局部变量,方法参数)。

java的每条线程都有自己的工作内存(一般对应CPU高速缓存),线程工作内存保存了该变量的主内存副本,线程对变量的操作都在自己的工作内存进行,线程、工作内存、主内存关系如图(图来自《深入理解java虚拟机第二版》-周志明)

java内存模型

CPU与高速缓存结构图(图来自《深入理解计算机系统》):
CPU与高速缓存结构图

2、内存间的交互操作

主内存与工作内存间的交互操作,即一个变量如何从主内存加载到工作内存,如何从工作内存写入到主内存。jmm定义了一下八种操作

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

如果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作,如果 要把变量从工作内存同步回主内存,就要顺序地执行store和write操作。注意,Java内存模型 只要求上述两个操作必须按顺序执行,而没有保证是连续执行。也就是说,read与load之 间、store与write之间是可插入其他指令的,如对主内存中的变量a、b进行访问时,一种可能 出现顺序是read a、read b、load b、load a。除此之外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

这8种内存访问操作以及上述规则限定,再加上稍后介绍的对volatile的一些特殊规定,就已经完全确定了Java程序中哪些内存访问操作在并发下是安全的。由于这种定义相当严谨,但又十分烦琐,实践起来很麻烦,这种定义有一个等效简单的判断原则——先行发生原则(happens-before原则),用来确定一个访问在并发环境下是否安全。

3、volatile规则

volatile是最轻量级的同步机制,性能优于synchronized,volatile有两个作用:

1、保证此变量对所有线程的可见性
2、禁止指令重排序优化

3.1、保证此变量对所有线程的可见性

保证变量对所有线程可见,线程对volatile变量的读操作之前必须从注册刷新变量值,写操作也需要写入主存,通过这种方式保证可见性。但是不以为只对volatile变量的运算是线程安全的。

public class Test{
    public static volatile count=0;
    public static void main(String[] args){
         Thread t1=new Thread(new Runnable(){ 
                @Override 
                public void run(){ 
                     for(int i=0; i<10000; i++){ 
                           count++;
                     }
                 }
             });
           Thread t2=new Thread(new Runnable(){ 
                @Override 
                public void run(){ 
                     for(int i=0; i<10000; i++){ 
                           count++; 
                     }
                 }
             });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(count);
    }
}

上面这段程序是多volatile变量count进行的累加操作,两个线程,优于count++不是原子操作,在多线程下仍然会有并发问题。

3.2、禁止指令重排序优化

java编译器对代码进行优化时会对没有依赖关系的语句顺序做调整,以便更快的执行程序,但是有时候这种优化会带来问题。
下面是一个典型的单例的例子:

public class Singleton{ 
     private volatile static Singleton instance; 
     public static Singleton getInstance(){ 
          if(instance==null){ 
                synchronized(Singleton.class){  
                      if(instance==null){ 
                            instance=new Singleton(); 
                      }
                }
           }
           return instance; 
     }
     public static void main(String[]args){ 
          Singleton.getInstance();
     }
}

对instance变量赋值部分编译后的字节码如下:

0x01a3de0f:mov$0x3375cdb0,%esi;……beb0cd75 33 ;{oop('Singleton')} 
0x01a3de14:mov%eax,0x150(%esi);……89865001 0000 
0x01a3de1a:shr$0x9,%esi;……c1ee09 
0x01a3de1d:movb$0x0,
0x1104800(%esi);……c6860048 100100 
0x01a3de24:lock addl$0x0,(%esp);……f0830424 00 ;*putstatic instance 
;- 
Singleton:getInstance@24

lock addl$0x0,(%esp);该操作相当于一个内存屏障
该操作是将ESP寄存器的值加0,是一个空操作,该操作保证CPU的cache写入内存。

volatile实现细节:

字节码层面:在变量前面加了ACC_VOLATILE标记

JVM层面,在读写操作前面增加屏障命令:

   StoreStoreBarrier
   volatile写操作
   StoreLoadBarrier

   LoadLoadBarrier
   volatile读操作
   LoadStoreBarrier

OS和硬件层面:
使用CPU内存屏障指令,通过屏障指令保证指令执行顺序,指令重排序无法越过内存屏障

 1. sfence: save  |写屏障, sfence指令前的写操作必须执行完后才能执行其后的写操作 
 2. lfence: load | 读屏障, lfence.......
 3. mfence: mix 读写屏障, mfence

杜若
70 声望3 粉丝