Java基础原理-Java JVM

深入理解JVM第十二章笔记

2020-03-17  本文已影响0人  Cool_Pomelo

深入理解JVM第十二章笔记

背景

为了充分压榨计算机处理器的性能,多任务处理在现代计算机操作系统中已经是一项必备技能了。

另外由于大部分的计算任务都不可能只靠处理器来单独“计算”完成,处理器需要与内存交互,如读取运算数据,存储运算结果等,这个IO操作是很难消除的。而如今的计算机的存储设备与处理器的运算速度有几个数量级的差距,所以需要在处理器与内存之间加入一层---高速缓存,用来将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就可以无需等待缓慢的内存读写了。

高速缓存的引入也带来了一个新的问题:缓存一致性:

多处理器系统,每个处理器都有自己的高速缓存,它们又共享同一个主存:

图1.png

当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致

为了解决一致性问题,需要各个处理器访问缓存时都遵循一些协议,在读写时根据协议来进行操作,这类协议有:MSI,MESI等等

所谓的“内存模型”,可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象,不同的物理机器有不一样的内存模型,JVM也有属于自己的内存模型。

Java内存模型

Java虚拟机规范定义了一种Java内存模型(JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

主内存与工作内存

JMM的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节,此处的变量与Java编程中所说的变量有所区别,它包括:

但不包括局部变量与方法参数,因为它们属于线程私有的。

JMM规定了所有变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线性对变量的所有操作(读取,赋值等)都必须在工作内存进行,不能直接读写主内存中的变量

线程,主内存,工作内存三者的交互关系:

图2.png

内存间交互操作

对于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存之类的实现细节,Java内存模型定义了8种操作来完成主内存与工作内存的读写交互,虚拟机实现保证每一种操作都是原子的,不可再分的

如果要把一个变量从主内存复制到工作内存,那就要顺序的执行read和load操作

如果要把变量从工作内存同步回主内存,就要顺序执行store和write操作

Java内存模型这2个操作必须顺序执行,但不保证连续执行,即在指令之间可以插入其它指令

但是Java内存模型规定了一些必要的规则

对于volatile型变量的特殊规则

当一个变量定义为volatile之后,它将具备两种特性:

原子性 可见性 有序性

大致可以认为基本数据类型的访问读写是具备原子性的

当一个线程修改了共享变量的值,其他线程能够立即得知这个修改

除了volatile,能够保证可见性的还有:

Java提供了volatile和synchronized保证有序性

先行发生原则

先行发生是指JMM中定义的两项操作直接的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括了:

等等

例子:



i = 1 // 线程A中执行


j = i // 线程B执行

i = 2 // 线程C执行

假设A中操作" i = 1"先行发生于B的操作" j = i",那么可以确定在B的操作执行后,变量j的值一定等于1,得出这个结论的依据:

现在把C考虑进去:

依旧保持A和B之间的先行发生关系,C出现在A和B之间,但C和B没有先行发生关系,那j的值就会出现不确定的情况,1或2都有可能:因为C对变量i的影响可能会被B观察到,也可能不会

下面是JMM中一些“天然的”先行发生关系:

在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,这里的“后面”指的是时间上的先后顺序。

对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”指的是时间上的先后顺序

Thread对象的start()方法先行发生于此线程的每一个动作。

线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生(即先中断,后发现被中断),可以通过Thread.interrupted()方法检测到是否有中断发生。

一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

若操作A先行发生于操作B,操作B先行发生于操作C,那么就可以得出操作A先行发生于操作C。

以上规则是Java语言“天然”存在的规则,无需同步手段(例:加synchronized)就能保证先行发生。

例子:



  private int value = 0;


    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

假设:

有线程A和B,A先调用了setValue(1),然后B调用了getValue()

那B收到的返回值是什么?

依次分析先行发生原则中的各项规则:

没有一个适用的先行发生原则,传递性也无从谈起,所以这里的操作不是线程安全的

Java与线程

线程的实现

线程的引入可以把一个进程的资源分配和执行调度分开,线程既可共享进程资源(内存地址、文件I/O等),也可独立调度(线程是CPU调度的基本单位)

实现线程有三种方式:

内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线 图3.png

程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。 每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(MultiThreads Kernel)。

程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。 这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型,如下图所示:

由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作,但是轻量级进程具有它的局限性:首先,由于是基于内核线程实现的,所以各种线程操作,如创建、 析构及同步,都需要进行系统调用。 而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。 其次,每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。

从广义上来讲,一个线程只要不是内核线程,就可以认为是用户线程(UserThread,UT),因此,从这个定义上来讲,轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作都要进行系统调用,效率会受到限制。

而狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。 用户线程的建立、 同步、 销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。 这种进程与用户线程之间1:N的关系称为一对多的线程模型,如下图所示:

图4.png

使用用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要用户程序自己处理。 线程的创建、 切换和调度都是需要考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”、 “多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至不可能完成。 因而使用用户线程实现的程序一般都比较复杂,除了以前在不支持多线程的操作系统中(如DOS)的多线程程序与少数有特殊需求的程序外,现在使用用户线程的程序越来越少了,Java、Ruby等语言都曾经使用过用户线程,最终又都放弃使用它。

线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式。 在这种混合实现下,既存在用户线程,也存在轻量级进程。 用户线程还是完全建立在用户空间中,因此用户线程的创建、 切换、 析构等操作依然廉价,并且可以支持大规模的用户线程并发。 而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。 在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N:M的关系,如下图所示,这种

图5.png

参考资料

<<深入理解Java虚拟机>>

上一篇 下一篇

猜你喜欢

热点阅读