六、Java内存模型与线程
Java内存模型
用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。 主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节操作。此处的变量包括实例字段、静态字段和构成数组对象的元素,不包括局部变量与方法参数。
1. 主内存与工作内存
- Java内存模型规定了所有的变量都存储在主内存中(此处的主内存与介绍物理硬件时的主内存名称一样,两者也可以互相类化,但此处仅是虚拟机内存的一部分)。
- 每条线程还有自己的工作内存(可与处理器高速缓存类比),线程的工作内存中保存了被该线程使用到的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。

2、内存间交互操作
-
关于主内存与本地内存之间具体的交互协议,即一个变量如何从主内存拷贝到本地内存、如何从本地内存同步回主内存之类的实现细节,JMM中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每种操作都是原子的、不可再分的(对于double和long类型的遍历来说,load、store、read和write操作在某些平台上允许有例外):
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独立的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的本地内存中,以便随后的load动作使用。
- load(载入):作用于本地内存的变量,它把read操作从主内存中得到变量值放入本地内存的变量副本中。
- use(使用):作用于本地内存的变量,它把本地内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign赋值):作用于本地内存的变量,它把一个从执行引擎接收到的值赋给本地内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于本地内存的变量,它把本地内存中的一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从本地内存中提到的变量的值放入到主内存的变量中。
-
如果把一个变量从主内存复制到工作内存,那就要顺序的执行read和load操作,如果要把变量从工作内存同步回主内存,就需要顺序地执行store和write操作。
3、对于volatile型变量的特殊规则
-
当一个变量定义为volatile之后,它具备两种特性:
- 保证此变量对所有线程的可见性,此处的可见性表示当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。volatile 虽然保证了可见性,但是 Java 里面的运算并非原子操作,导致 volatile 变量的运算在并发下一样是不安全的。而 synchronized 关键字则是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得线程安全的。
- 禁止指令重排序优化,普通的变量仅仅保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而并不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
-
内存屏障:指重排序时不能把后面的指令重排序到内存屏障之前的位置。只有一个CPU访问内存时,并不需要内存屏障;但如果有两个或更多CPU访问同一快内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了。
4、原子性、可见性与有序性 java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的。
- 原子性: 由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的(例外是long和double的非原子性协定),在synchronized块之间的操作也是具备原子性的。
- 可见性: 当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此。volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此volatile可以保证可见性。除了volatile以外,synchronized和final也能实现可见性。synchronized保证unlock之前必须先把变量刷新回主内存。final修饰的字段在构造器中一旦完成初始化,并且构造器没有把this的引用传递出去,那么其他线程就能看到final字段的值。
- 有序性: java的有序性跟线程相关。如果在线程内部观察,会发现当前线程的一切操作都是有序的。如果在线程的外部来观察的话,会发现线程的所有操作都是无序的。因为JMM的工作内存和主内存之间存在延迟,而且java会对一些指令进行重新排序。volatile和synchronized可以保证程序的有序性,volatile关键字本身就包含了禁止指令重排序的含义;synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的。
5、先行发生原则(happens-before)
- 它是判断数据是否存在竞争、线程是否安全的主要依据。依靠这个原则,我们可以通过几条规则来解决两个线程之间是否存在冲突的所有问题。
- 先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
- 规则:
- 程序次序规则(Program Order Rule):在一个线程内,程序的执行规则跟程序的书写规则是一致的,从上往下执行。
- 管程锁定规则(Monitor Lock Rule):一个Unlock的操作肯定先于下一次Lock的操作。这里必须是同一个锁。同理我们可以认为在synchronized同步同一个锁的时候,锁内先行执行的代码,对后续同步该锁的线程来说是完全可见的。
- volatile变量规则(volatile Variable Rule):对同一个volatile的变量,先行发生的写操作,肯定早于后续发生的读操作。
- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的没一个动作
- 线程中止规则(Thread Termination Rule):Thread对象的中止检测(如:Thread.join(),Thread.isAlive()等)操作,必行晚于线程中所有操作。
- 线程中断规则(Thread Interruption Rule):对线程的interruption()调用,先于被调用的线程检测中断事件(Thread.interrupted())的发生。
- 对象终结规则(Finalizer Rule):一个对象的初始化方法先于一个方法执行Finalizer()方法。
- 传递性(Transitivity):如果操作A先于操作B、操作B先于操作C,则操作A先于操作C。
java与线程
1、线程的实现
-
线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。
-
实现线程主要有三种方式 (1). 使用内核线程实现: 内核线程就是直接有操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。这种轻量级进程与内核线程之间为1:1的关系。
- 由于内核线程的支持,每一个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程的继续工作。各种线程操作,都需要进行系统调度,代价较高。并且轻量级进程会消耗一定量的内核资源。
(2). 使用用户线程实现: 完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。进程与用户线程之间为1:N的关系。 - 优势在于不需要系统内核的帮助;劣势也在于没有系统内核的帮助,所有的线程操作都需要用户程序自己处理。
(3). 使用用户线程加轻量级进程混合实现: 用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等依然廉价,并且可以支持大规模的用户线程并发。轻量级进程则作为用户线程和内核线程之间的桥梁,用户线程的系统调用通过轻量级线程来完成,降低了整个进程被完全阻塞的风险。用户线程与轻量级进程之间的关系为N:M。
2、java线程调度
- 线程调度是指系统为线程分配处理器使用权的过程主要有两种方式。
-
协同式线程调度:线程的执行时间由线程本身控制,线程把自己的工作执行完后,要主动通知系统切换到另外一个线程上。
- 好处是实现简单,没有线程同步问题。坏处是线程执行时间不可控,可能会一直处于阻塞。
-
抢占式线程调度:每一个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。
- 不会产生由于一个线程错误,导致的阻塞问题。
-
3、状态转换
- Java 线程(为什么说是 Java 线程呢?因为操作系统线程状态和这 6 个状态还有些许差异)一共有 6 种状态,Thread 类中专门定义了一个枚举类 State 来表示这 6 种状态。 (1). NEW(新建): 创建了一个 Thread 实例,还没有调用其 start() 方法,此时线程处于新建状态。 (2)RUNNABLE(运行): 运行状态包含了 Ready 和 Running 两种状态。什么意思呢?Ready 状态的意思就是我已经准备好一切,只要让我用 CPU 我就能 Running,即正在等待 CPU 分配执行时间;Running 状态就是得到了 CPU 的执行时间,正在运行。 (3)BLOCKED(阻塞): 线程 A 进入同步区域的时候,需要获取一个排他锁,若此时排他锁正在被另一个线程 B 占有,线程 A 将进入阻塞状态。 (4)WAITING(无限期等待): 处于这种状态的线程不会被分配 CPU 执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态:没有设置 Timeout 参数的 Object.wait() 方法;没有设置 Timeout 参数的 Thread.join() 方法;LockSupport.park() 方法。 (5)TIMED_WAITING(限期等待): 处于这种状态的线程也不会被分配 CPU 执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:Thread.sleep() 方法;设置了 Timeout 参数的 Object.wait() 方法;设置了 Timeout 参数的 Thread.join() 方法;LockSupport.parkNanos() 方法;LockSupport.parkUntil() 方法。 (6)TERMINATED(结束): run() 方法执行完成,线程结束执行,此时的线程状态。
