Java内存模型(一)
一、概述
Java内存模型即Java Memory Model(JMM)
,JVM
规范试图通过JMM
来屏蔽各种硬件和操作系统的内存访问差异,实现让Java程序在各种平台下都能达到一致的内存访问效果,规避像C、C++等主流编程语言直接使用物理硬件和操作系统的内存模型,因为不同平台上内存模型的差异可能导致并发访问的时经常出错,不得不针对不同平台编写不同程序的问题,实现一次编译,到处运行的设计思想。
二、Java与线程
并发编程
多任务和高并发是现代计算机系统必备的功能,是衡量计算机处理器的能力重要指标。为了减少在磁盘I/O、网络通信、或者数据库访问等耗时操作上造成的计算能力上的资源浪费,多任务处理是其最常用的手段。由于计算机的存储设备与处理器的运算能力之间存在着巨大差距,所以现代计算机系统都不得不加入读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中没这样处理器就无需等待缓慢的内存读写了。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存,如下图所示:多个处理器运算任务都涉及同一块主存,需要一种协议(MSI
、MESI
、MOSI
等)可以保障数据的一致性。
Java虚拟机内存模型中定义的内存访问操作与硬件的缓存访问操作是具有可比性的。
线程的基本概念
线程是比进程更轻量级的调度执行单位,线程可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址,文件I/O等),又可以独立调度(线程是CPU的调度基本单位)。
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用多线程模式不能用多进程模式。
实现线程主要有三种方式:内核线程实现、用户线程实现 和 使用用户线程加轻量级进程混合实现。
内核线程
内核线程(Kernel-Level Thread KLT
) 就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器(Thread Scheduler
)对线程进行调度,并负责将线程的任务映射到处理器上,每个内核线程可以视为内核的分身,这样操作系统就有能力同事处理多件事情,支持多线程的内核就叫做多线程内核(Multi-Threads Kernel
)。程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口 — 轻量级进程(Light Weight Process LWP
)。轻量级进程
轻量级进程就是我们通常意义上的线程,由于每个LWP
都与一个特定的内核线程关联,因此每个LWP
都是一个独立的线程调度单元。即使有一个LWP
在系统调用中阻塞,也不会影响整个进程的执行,但是轻量级进程具有局限性:
首先,大多数LWP
的操作,如 建立、析构以及同步都需要进行系统调用,系统调用的代价相对较高,需要在user mode
和kernel mode
中切换。
其次,每个LWP
都需要有一个内核线程支持,因此LWP
要消耗内核资源(内核线程的栈空间),因此一个系统不能支持大量的LWP
。
- 用户线程
LWP
虽然本质上属于用户线程,但LWP
线程库是建立在内核之上的,LWP
的许多操作都要进行系统调用,因此效率不高。而这里的用户线程(User Thread, UT
)指的是完全建立在用户空间的线程库,用户线程的建立,同步,销毁,调度完全在用户空间完成,不需要内核的帮助。因此这种线程的操作是极其快速的且低消耗的。
上图是最初的一个用户线程模型,从中可以看出,进程中包含线程,用户线程在用户空间中实现,内核并没有直接对用户线程进程调度,用户线程之间的调度由在用户空间实现的线程库实现,其缺点是:一个用户线程如果阻塞在系统调用中,则整个进程都将会阻塞。
- 使用用户线程加轻量级进程混合实现
用户线程库还是完全建立在用户空间中,因此用户线程的操作还是很廉价,因此可以建立任意多需要的用户线程。操作系统提供了LWP
作为用户线程和内核线程之间的桥梁。LWP
还是和前面提到的一样,具有内核线程支持,是内核的调度单元,并且用户线程的系统调用要通过LWP
,因此进程中某个用户线程的阻塞不会影响整个进程的执行。用户线程库将建立的用户线程关联到LWP
上,LWP
与用户线程的数量不一定一致。当内核调度到某个LWP
上时,此时与该LWP
关联的用户线程就被执行。
当多个线程访问一个对象时,如果不用考虑这个线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
线程间的通信
通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。
线程之间的同步
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。
线程调度
线程调度指系统为线程分配处理器使用权的过程,主要有两种调度方式:
- 协同式调度(
Cooperative Threads-Scheduling
):线程执行时间由线程自身控制,线程把自己的工作执行完成之后才主动通知系统切换到另外的线程。如果一个线程出错会阻塞整个系统的运行。 - 抢占式调度(
Preemptive Threads-Scheduling
):线程的执行时间由系统根据线程的优先级来分配,优先级越高的线程越容易被系统选择执行。如果一个线程出错不会阻塞其他线程。
参考文献
- 《深入理解Java虚拟机:JVM高级特性与最佳实践》,周志明著
- 深入理解java内存模型系列文章
- 全面理解Java内存模型