概览
JVM从本质上来讲,其本身就是一个虚拟计算机+操作系统,也正因为它有自己的内存划分规则、程序运行规则等内容,才使得JVM可以跨平台运行。在操作系统中有一个重点的章节为调度策略,对于JVM来说,其线程的调度策略为抢占式,当涉及到生产者消费者问题时,通过同步工具可以实现协同式调度。虽然对于JVM的线程来说,具有优先级这个属性,这个属性官方不建议使用,因为无法保证线程调度策略严格按照优先级来执行,例如线程池中的线程调度策率就取决于采用的阻塞队列。
如果从JVM的角度去理解线程,则需要重点关注线程同步工具的底层实现和JVM的内存模型。
操作系统线程
线程是二十世纪六十年代操作系统引入作为独立运行的基本单位,操作系统的资源利用率得到了极大的提升。首先进程是资源申请的独立单位,将系统调度的最小单位转为线程,可以在创建、撤销和切换时造成的开销缩小,也就是将资源和调度相分离,来达到效率最大化。线程只拥有一些在运行中必不可少的资源(如程序计数器、一组寄存器和栈),它与同一进程的其他线程共享该进程拥有的全部资源。,线程切换时只需要保存和设置少量的寄存器内容,并不涉及存储器管理,所以线程切换开销低。
每个线程有一个thread结构,即线程控制块,用于保存自己私有的信息,主要有以下几个基本部分:状态和调度信息、线程标识信息、现场信息、线程私有存储区、指针。
线程与进程的比较
线程的实现
- 内核级线程
在完全内核级线程环境中,线程管理的全部工作由操作系统内核在内核空间中实现,如线程创建、结束、同步等系统调用。内核调度空间以线程为单位。当线程被创建,内核同时为进程创建第一个核心级线程,运行用户的初始程序;以后可调用创建线程的系统调用,创建新的线程。内核级线程即运行用户程序,在自陷或中断进管又运行核心程序
内核级线程的优点:在多处理器上,同一进程内线程可以并发执行,如果进程中的一个线程阻塞,内核能够调度同一进程的其他线程占有处理器;内核级线程的数据结构和栈堆均较小,线程切换快,从而可以提高处理器的效率;内核级自身可以用多线程技术实现,提高了系统的并行性和执行速度。
内核级线程的缺点:应用程序的线程运行在用户空间,而线程调度和管理在内核空间,即使在同一进程在运行,当对线程的控制需要从一个线程传送到另一个线程时,也要通过用户态到核心态,再从核心态到用户态的转换,系统开销大。核心级线程表格占用系统空间。
- 用户级线程
用户级线程只用于用户级,线程的创建、撤销及切换都不利于系统调度实现,因而这种线程与内核无关,内核也不知道线程的存在。
用户级线程有以下优点:线程的切换不需要内核特权方式,无需修改操作系统的核心,因为所有线程管理的数据结构均在单个进程的用户空间中,管理线程切换的线程库也运行在用户空间。进程对线程的管理不需要切换到内核方式,这节省了切换的时间和内核资源。
用户级线程的主要缺点:线程因I/O等原因阻塞于内核,多线程调度器不知道,则该线程所在的进程的所有线程都将被阻塞,使得处理器空闲,资源浪费;系统在进程调度时分配给进程的处理器时间需要由所有的线程分享,不能做到同一进程在多CPU上并行。
- 混合式线程
在混合式线程实现中,不但内核支持内核级线程的建立、调度和管理,而且允许用户应用程序建立、调度和管理用户级程序。
线程的模型
- 一对一模型
一对一模型将一个用户级线程映射到一个核心级线程。创建一个用户级线程,需要创建一个内核级线程与之对应。这种情况下,如果一个线程阻塞,系统允许调度另一个线程运行。多处理器系统可以实现多个线程并行执行,系统效率高。缺点是每创建一个用户级线程,则需要创建一个内核级线程,系统的线程数目多,开销大,在实现中应该限制线程数 - 多对一模型
多对一模型将多个用户级线程映射到一个内核级线程,这多个用户级线程属于一个进程,运行在进程的用户空间,对线程的管理和调度也在用户空间上完成,只有当用户级线程需要访问内核时,才将其映射到一个内核线程上,但每次只允许一个下次讷航映射,该模型的优点是线程管理等都在用户空间完成,节约系统资源。缺点是如果一个线程阻塞,属于同一个进程的所有线程都阻塞,在多系统处理器中,一个进程的多个线程无法并行执行,系统效率低。 - 多对多模型
多对多模型将多个用户级线程映射到多个内核级线程,为了节约系统资源,与用户级线程对应的内核级线程数不会超过用户级线程数。内核级线程数可以通过特殊应用或者特殊及其指定。一个应用程序在多处理器系统中分配内核级线程数可以多于但处理器系统。
JVM的线程与内存模型
想要理解JVM的线程与内存模型只有操作系统原理的只是是远远不够的,关于内存的分区的还需要了解计算机组成原理的基本知识,因为JVM除了有操作系统的特质外还有硬件无关性的特质,这决定了它将具体的寄存器抽象化,以另一种形式展现(栈),JVM的内存模型这部分内容应该结合这计算机组成原理来理解。
主内存和工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
变量:包括了实例字段、静态字段和构成数组元素的对象,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然不会存在竞争关系。
举个栗子:
public class TestWay {
static int a=5;
public static void solution(int x){
x=x+1;
a=a+1;
System.out.println("方法内:"+x);
System.out.println("方法内:"+a);
}
public static void main(String[] args) {
int x=5;
solution(x);
System.out.println("主方法:"+x);
System.out.println("主方法:"+a);
}
}
Java规定了所有的变量都存储在主内存中,但每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到变量的内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者互相交互。
内存间的交互操作
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程的独占状态。
- read(读取):作用于主内存的变量,他把一个变量的值从主内存传输到线程的工作内存中,以便以后的load动作使用。
- load(读取):作用于工作内存的变量,他把read操作从主内存中得到的变量值放入到工作内存的变量副本中
- use(使用):作用于工作内存的变量,他把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码时将会执行此操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,他把工作内存中的一个变量的值传送给主内存中,以便有的write操作使用。
- wirte(写入):作用于主内存的变量,他把store操作从工作内存中得到的变量值放入主内存的变量中。
- unlock(解锁):作用于主内存的变量,他把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
配合八个基本方法的操作,当然还有八个基本规则
- 不允许read和load、strore和write这些操作单一出现,不允许一个变量从内存读取了但工作内存不接受,或这哦仓工作内存发起了写回,但是主内存不接受的情况。
- 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存
- 不允许一个线程无原因的(没有发生任何assign操作)把数据从线程的工作内存同步会主内存。(数据是来旅游的?)
- 一个新的变量同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量,就是对一个变量实施use,store操作之前,必须先执行assign和load操作。
- 如果对一个变量执行了lock操作,那将会清空工作内存的此变量的值,在执行引擎室用这个变量前,需要重新load和assign操作初始化变量的值
- 如果一个变量事先没有被lock锁定,那就不允许对它执行unlock操作,也不允许unlock一个被其他线程锁定住的变量
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、wirte操作)
Volatile的特殊规则
Volatile是JVM提供的最轻量级的同步机制,它的实现源于虚拟机对Java内存模型的一系列特殊的访问规则,当一个变量被定义为Volatile,那么它将具有两种特性,第一是保证这个此变量对其他线程来说是可以立即得知的,而普通的变量则不能做到这一点,普通的变量值在线程间来回传递需要通过主内存来完成,还有一个特性就是禁止指令重排序优化,普通的变量仅仅会保证该方法执行时所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量操作的顺序与程序代码中的执行顺序一致。
问题:
Volatile虽然是同步工具,但相对于Synchronized和Lock类来说还是太图样图森破。
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束。
当这两类情况出现,仍需要加重锁来确保线程同步的正确性。
Volatile的特殊规则:
- 只有当线程T对变量V执行的前一个动作是Load的时候,线程T才能对变量V执行use操作,并且,只有当线程T对变量V执行的最后一个动作是use操作时,线程T才能对变量V执行load操作。实现了线程T对变量V的use动作与load动作相关联,必须连续出现在一起。
这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新值,用于保证能看见其他线程对变量V所做的修改后的值 - 只有当线程T对变量V的操作执行的前一个动作是assign的时候,线程T才能对变量V执行store操作;并且,至于当线程T对变量V执行的后一个操作时Store时,线程才能对变量V执行assign操作。线程T对变量V的操作assign操作可以被认为适合线程T对变量的V的Store、write动作相关联,必须连续一起出现
这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存,用于保障其他线程可以看到自己对变量V所做的修改。 - 要求Volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同。
模型特征
- 原子性
- 可见性
- 有序性
先行发生原则
先行并发原则是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于B,发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中的共享变量的值、发送消息、调用方法等。