引言
内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写的过程抽象。不同架构的物理机器有着不同的内存模型,java用定义一种内存模型来屏蔽硬件和操作系统的内存访问差异。
主内存与工作内存
java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存到内存,从内存中取变量这样的底层细节。这里的变量指的是线程共享的变量。
JMM(java内存模型)规定了所有变量都存在主内存中,每条线程有自己的工作内存,工作内存保存了在主存中使用到变量的拷贝。线程对变量的操作都在自己的工作内存中进行,无法访问其他线程的工作内存。
JMM定义了以下8种操作完成主内存和工作内存间交互,这些都是原子操作:
lock:将主内存的一个变量标识为一条线程独占的状态。
unlock:释放主内存中独占变量。
read:将主内存中的变量传输到线程的工作内存。
load:将read取得的变量存到工作内存的变量副本中。
use:将工作内存中的变量传递给执行引擎。
assign:给工作内存中的变量赋值。
store:将工作内存中的变量值传到主内存。
write:将从工作内存中传输过来的变量放入主内存的变量中。
将变量从主内存中复制到工作内存就要顺序执行read、load;相反则是store、write。
同时还有如下规则:
- 不允许read、load、store、write单独出现。
- 变量在内存空间改变后必须将变化同步回主内存。
- 工作内存中的变量没有被改变就不用同步回主内存。
- 一个变量在同一时刻只能被一个线程lock。
- 如果对一个变量进行lock操作,在使用这个变量前需要load或assign这个变量。
- 有了lock才能进行unlock,不能unlock其他线程锁定的变量。
- 在unlock之前需要将变量同步回主内存。
其实上面的一部分操作主要是完成工作内存间的内存一致性。
volatile
volatile关键字java虚拟机提供的轻量级同步机制。
volatile保证了变量对所有线程的可见性,但是变量不一定是线程安全的。volatile适合以下场景:
- 运算结果不依赖变量当前值,或者能确保只有单一线程修改变量值。
- 变量不需要与其他状态变量共同参与不变约束。
volatile还有一个作用是保证禁止指令重排序。as-if-serial语义保证了重排序不会改变单线程的执行结果。但是在并发中重排序是会干扰执行结果的。
java内存模型要求以上的8个操作都具有原子性,对long和double这个两个64位的数据类型可以分为两次对32位数据的操作,如果修饰了volatile那么就会对这个两个数据进行原子操作,不过还是建议虚拟机对64位数据进行原子操作,所以大多数商用虚拟机都实现了对64位数据的原子操作。
线程
线程是操作系统基本调度单位,Thread类中的大部分方法都声明了native,也就是用本地方法实现的。
实现线程主要有3种方式:使用内核线程、使用用户线程、使用用户线程加轻量级进程混合实现。
- 使用内核线程实现
内核线程由操作系统内核支持,内核将线程的任务映射到处理器上。程序一般不是直接使用内核线程,而是使用内核线程的高级接口(轻量级进程),每一个轻量级进程由一个内核线程实现。不过轻量级进程需要在用户态和内核态之间频繁切换,代价高,还会消耗内核资源。 - 使用用户线程实现
狭义上的用户线程是完全建立在用户空间上的,线程的建立、同步、调度、销毁和调度完全在用户态完成,不需要内核帮助。但是很多东西都需要用户程序自己处理,甚至有些不能实现,所有现在用用户线程的程序的越来越少。 - 使用用户线程加轻量级进程混合实现
把轻量级进程作为用户线程和内存线程之间的桥梁,拥有了轻量级进程和用户线程的优点。
java线程调度
主要的线程调度有协同式线程调度和抢占式线程调度。
协同式调度中,线程的执行时间由线程本身控制,当线程自己的工作执行完后,主动通知系统切换到另外一个线程。 这种方式如果线程编写不对会相当不稳定。
抢占式调度由系统来分配执行时间,java使用的就是这种,根据线程优选级进行调度。