为了解决计算机存储设备与处理器的运算速度差异而导致的服务性能的问题,通过加入高速缓存,以减少内存读写,提高运行效率。但这引入了一个新的问题:缓存一致性。也就是说,对于复制到缓存中的数据,可能在运算过程中发生改变,那同步到内存中,该以谁的缓存数据为准却不得而知。
1. Java内存模型
Java内存模型的主要目标是定义程序中变量的访问规则(这种规则协议保证了数据一致性)。即在虚拟机中将变量存储到主内存或者将变量从主内存取出这样的底层细节。Java内存模型规定了所有变量都存储在主内存中,每条线程还有自己的工作内存。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。
工作内存可类比于高速缓存,开辟于高速缓存中;主内存可类比于硬件主内存,开辟于硬件主内存中。从两内存的性质还可以推出:工作内存线程私有,可对应于虚拟机栈中的部分区域(栈帧的局部变量表私有),主内存可被共享对应于Java堆中的对象实例数据部分。
1.1 内存间交互操作
Java内存模型定义了八种操作完成内存与工作内存的交互,这八种操作及执行这些操作的规则保证了数据的一致性。
- lock(锁定)
作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量 - unlock(解锁)
作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定 - read(读取)
作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用 - load(载入)
作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到(加载)工作内存的变量副本中 - use(使用)
作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作 - assign(赋值)
作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作 - store(存储)
作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用 - write(写入)
作用于主内存的变量,把store操作从工作内存中得到的变量的值写入主内存的变量中
1.2 原子性、可见性与有序性
- 原子性
由Java内存模型来直接保证原子性的变量操作包括read、load、use、assign、store、write这6个动作,即这些操作是原子的,不可分的。 - 可见性
可见性是指一个线程修改了一个变量的值后,其他线程立即可以感知到这个值的修改。 - 有序性
有序性可概括为:如果在本线程内部观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句说的就是“线程内表现为串行的语义”,后半句值得是“指令重排序”现象和主内存与工作内存之间同步存在延迟的现象。
指令重排优化是为了使处理器内部的运算单元能尽量被充分利用而对指令打乱。
1.3 先行发生原则
先行发生原则用来确定一个访问在并发环境下是否安全。先行发生原则是Java内存模型中定义的两个操作之间的偏序关系。比如说操作A先行发生于操作B,那么在B操作发生之前,A操作产生的“影响”都会被操作B感知到。这里的影响是指修改了内存中的共享变量、发送了消息、调用了方法等。Java内存模型下一些天然的先行发生关系,无须任何同步器协助就已经存在:
- 程序次序原则
在一个线程内部,按照代码的顺序,书写在前面的先行发生与后边的。或者更准确的说是在控制流顺序前面的先行发生与控制流后面的,而不是代码顺序,因为会有分支、跳转、循环等。 - 管程锁定规则
一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须注意的是对同一个锁,后面是指时间上的后面 - volatile变量规则
对一个volatile变量的写操作先行发生与后面对这个变量的读操作,这里的后面是指时间上的先后顺序 - 线程启动规则
Thread对象的start()方法先行发生与该线程的每个动作。当然如果你错误的使用了线程,创建线程后没有执行start方法,而是执行run方法,那此句话是不成立的,但是如果这样其实也不是线程了 - 线程终止规则
线程中的所有操作都先行发生与对此线程的终止检测,可以通过Thread.join()和Thread.isAlive()的返回值等手段检测线程是否已经终止执行 - 线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。 - 对象终结规则
一个对象的初始化完成先行发生于他的finalize方法的执行,也就是初始化方法先行发生于finalize方法 - 传递性
如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。
1.4 volatile变量与synchronized
下面说一下被volatile修饰的变量及synchronized修饰的代码块或方法具有的性质。
- volatile变量
volatile是Java虚拟机提供的最轻量级的同步机制。 volatile类型的变量具有两种性质:保证对所有线程的可见性以及禁止指令重排序优化。虽然它具有线程可见性但并不能保证在并发下是安全的。在不符合以下两条规则的运算场景中仍需要加锁来保证原子性。
(1)运算结果并不依赖变量的当前值,或者能确保只有单一的线程修改变量的值。
(2)变量不需要与其他的状态变量共同参与不变约束 - synchronized修饰的代码块或方法
synchronized修饰的代码块或方法是并发安全的,该关键字在指令级加入了monitorenter和monitorexit指令来隐式地使用lock和unlock操作。在执行monitorenter指令时,首先获取对象的锁,若已获得或者该对象没锁定,则把锁的计数器加1,执行monitorexit指令时,锁的计数器减1。当计数器为0时锁会被释放。若获取对象锁失败,则当前线程进入阻塞等待,直到对象锁被释放。
2. Java与线程
2. 1 线程的实现
在Java语言中,每个已执行start()且还未结束的java.lang.Thread类的实例就代表了一个线程。实现线程主要有三种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。
- 使用内核线程实现
内核线程(Kernel-Level Thread, KLT) 就是由操作系统内核支持的线程,内核通过操纵调度器对线程进行调度。程序一般不会直接使用内核线程,而是去使用内核线程的一种高级接口-轻量级进程(LWP),轻量级进程就是我们通常意义上所讲的线程。这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型。Java线程的实现是基于1:1的模型。
-
使用用户线程实现
用户线程(User Thread, UT)指的是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。因此操作比内核线程更快速,并可以支持更大的线程数量。这种进程与用户线程之间1:N的关系称为一对多的线程模型。
由于用户线程没有系统内核的支援,所有的线程操作都需要用户程序自己处理。例如线程的创建、切换和调度都是需要考虑的问题,所以使用用户线程的程序一般都比较复杂。 -
使用用户线程加轻量级进程混合实现
操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁。这样可以使用内核提供的线程调度功能及处理器映射,用户线程调用通过轻量级线程来完成,降低了整个进程被完全阻塞的风险。用户线程与轻量级进程的数量比是不定的,即为N:M的关系,这种就是多对多的线程模型。
2. 2 Java线程调度
线程调度是指系统为线程分配处理器使用权的过程。主要有两种调度方式:
- 协同式线程调度
线程的执行时间由线程本身控制,线程执行完后,主动通知系统切换到另一个线程上。缺点是执行时间不可控。 - 抢占式线程调度
每个线程将由系统分配执行时间,java使用的线程调度方式就是抢占式调度
2. 3 状态转换
Java语言定义了6种状态,同一时刻,线程有且仅有其中的一种状态。状态分别为:新建,运行,无限期等待,限期等待,阻塞,结束。
新建:创建后尚未启动的线程
运行:包括正在执行以及等待CPU分配执行时间的线程
无限期等待:等待被其他线程唤醒的线程
限期等待:等待一定时间后系统自动唤醒
阻塞:等待获取一个排他锁
结束:线程执行结束