转载自 http://freecoder.com.cn/?p=289
一、JAVA内存模型
1.1 主内存与工作内存
- Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory)。
- 线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写内存中的变量。
- 不同的线程之间也无法直接访问对方工作内存中的变量,所以线程间变量值得传递需要通过主内存来完成。
1.2 内存间交互操作
Java内存模型中定义了8种操作,来规定一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节。虚拟机保证下面的操作都是原子的、不可再分的(double和long类型例外)。
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它吧一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
上述8中基本操作,必须满足如下规则:
- 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起会写了但主内存不接受的情况出现。
- 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign操作初始化变量的值。
- 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把次变量同步回主内存中(执行store、write操作)。
1.3 对volatile、long和double型变量的特殊规则
volatile型变量
volatile型变量具备两种特性:
- 保证此变量对所有线程的可见性。保证新值能立即同步到主内存,以及每次使用前立即从主内存刷新。而普通变量的值在线程间传递需要通过主存来完成,例如:线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在A回写完成后再从内存进行读取操作,新变量的值才会对线程B可见。
- 禁止重排序优化。如:
如果在编译阶段,指令被重排序优化,那么initialized = true,可能会在前面线程加载配置文件之前执行。这样就会出现还没加载配置文件,后面的线程就开始按照配置文件已经加载完的情况开始执行。
long和double型变量
Java内存模型允许将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现可以不保证64位数据类型的load、store、read和write这4个操作的原子性。
1.4 原子性、可见性与有序性
原子性(Atomicity):除了原子性变量操作read,load,assign,use,store和write,Java内存模型提供了lock和unlock操作来保证更大范围的原子性。尽管虚拟机并没有把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地来使用这两个操作,这两个字节码指令反映到Java代码中就是同步块——synchronize关键字。
可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。除了volatile之外,synchronized和final也能实现可见性
有序性(Ordering):如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
Java语言停工了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronize则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获得的。
1.5 先行发生原则
二、JAVA与线程
2.1 线程的实现
实现线程主要有两种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。
2.1.1 使用内核线程实现
内核线程(Kernel-Level Thread,KLT)就是直接又操作系统内核支持的线程,这种线程由内核来完成线程切换,通常通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(Multi-Threads Kernel)。
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Wight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才有轻量级进程。这种轻量级进程与内核之间1:1的关系,称为一对一的线程模型,如下图。
由于内核线程的支持,每个轻量级进程都称为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作。
轻量级进程局限性:
- 基于内核线程实现,各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。
- 每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。
2.1.2 使用用户线程实现
狭义上的用户线程(User Thread,UT)指的是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。
优点:不需要切换到内核态,操作是非常快速且低消耗的,也可以支持规模更大的线程数量。
缺点:由于操作系统只把处理器资源分配到进程,那些诸如“阻塞如何处理”、“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来异常困难,甚至不可能完成。
应用:部分高性能数据库中的多线程就是由用户线程实现的。
2.1.3 使用用户线程加轻量级进程混合实现
混合实现下,即存在用用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N:M的关系,称为多对多的线程模型。
应用:许多UNIX系列的操作系统,都提供了N:M的线程模型来实现。
2.1.4 Java线程的实现
对于Sun JDK来说,它的Windows版与Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程中,因为Windows和Linux系统提供的线程模型就是一对一的。
而在Solaris平台中,由于操作系统的线程特性可以同时支持一对一即多对多的线程模型,所以在Solaris版的JDK中可以配置使用哪种线程模型。
2.2 Java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有:
- 协同式线程调度。线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。
- 抢占式线程调度。每个线程又系统来分配执行时间,线程的切换不由线程本身来决定。
2.3 状态转换
Java语言定义了5种线程状态:
- 新建(New):创建后尚未启动的线程处于这种状态。
- 运行(Runable)
- 无限期等待。
- 限期等待。
- 阻塞