JAVA多线程编程——JAVA内存模型
一、何为“内存模型”
内存模型描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节,对象最终是存储在内存里面的,但是编译器、运行库、处理器或者系统缓存可以有特权在变量指定内存位置存储或者取出变量的值。
二、JMM(Java Memory Model)即Java内存模型的作用
- JMM的最初目的是为了能够支持多线程程序。JMM使得每一个线程就像运行在不同的机器、不同的CPU或者本身就不同的线程上一样;
- JMM定义了Java语言针对内存的一系列相关规则。对于CPU本身而言,一个CPU不能直接访问其它CPU的寄存器,因此JMM必须通过某种定义规则来使得线程和线程在工作内存中进行相互调用,从而实现一个CPU对其它CPU、或者说一个线程对其它线程的内存中资源的访问;
- 虽然JMM设计之初是为了能够更好地支持多线程,但是JMM的应用和实现并不局限于多处理器,对于单CPU的系统而言,在JVM编译器编译Java程序的时候,以及运行时执行该程序的时候,这种规则也是有效的;
- JMM定义了线程与主存之间的抽象关系:每个线程可以被抽象为一块工作内存,程序中所有的共享变量都在主存中定义并存储,工作内存不能直接使用主存中的共享变量,如果要使用,工作内存必须对主存中的共享变量进行读取和拷贝,然后对拷贝过来的变量副本进行操作,最后将操作后的变量结果回写到主存中。大多数JMM规则在实现的时候,必须保证主存和工作内存之间进行通信,而且不能违反内存模型本身的结构。这是在设计语言的时候必须考虑到的针对内存的一种设计方法。
作为Java程序员,我们需要知道的是,Java对内存的管理不需要人为操作,因为Java本身就拥有了一套自动的内存管理策略,这是Java相对与其它一些语言在进行内存管理上具备的一种优势。
三、线程间通信机制
在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
- 共享内存。线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信;
- 消息传递。线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。
Java在实现线程间通信时采用的是共享内存的方式,因而Java线程之间的通信总是隐式的,整个通信过程对程序员完全透明。如果我们在编写多线程程序的时候不理解这种隐式的通信机制,很可能会遇到各种奇怪的并发问题。
四、主存与工作内存
上面我们将每个单独的线程抽象为一块工作内存,主存与线程之间的关系也就被抽象成了主存与工作内存的关系,这种关系用图可表示为:
JMM定义了8中主存与工作内存之间的操作:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态;
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用;
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作;
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作;
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。
JMM还规定了在执行上述八种基本操作时,必须满足如下规则:
- 不允许read和load、store和write操作之一单独出现;
- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中;
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中;
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作;
- 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现;
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值;
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量;
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
五、关于重排序
在Java程序的执行过程中,编译器和处理器会通过对指令进行重排序来优化程序的执行效率。重排序分为三种:
1.编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
2.指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
3.内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从java源代码到最终实际执行的指令序列,会分别经历上面三种重排序。