JVM篇·JVM内存模型与线程

Java内存模型与线程

本文为《深入理解Java虚拟机_JVM高级特性与最佳实践·周志明》学习笔记

背景知识

TPS(每秒事务处理数):代表着1s内服务端平均处理响应的请求总数。

在相同的任务下,TPS越高,代表程序线程并发协调有条不紊,效率高;TPS越小,线程之间频繁征用数据,互相阻塞以及死锁,降低并发能力;

高速缓存:内存与处理器的桥梁,解决了之间读写速度不一致的问题,同时需要保证缓存一致性(遵守相关协议解决:MSI,MESI,MOSI等)。

共享内存多核系统:多路处理器系统每个处理器都有自己的高速缓存,同时又共享同一主内存;

JAVA内存模型

背景: C/CPP是采用了物理硬件和OS的内存模型,JAVA想屏蔽各种硬件和系统的内存访问差异,实现各个平台下都能达到内存访问一致的效果。

主内存&工作内存

目的:定义程序中的各种变量(实例字段静态字段和构成数组对象的元素不包含局部变量与参数方法,后者是线程私有)的访问规则;为了避免竞争问题;

规定:所有的变量都存储在JVM主内存中,线程的工作内存中保存了该线程使用变量的主内存副本;线程对变量的所有操作(d,w)都必须在工作内存中进行,不能直接读写主内存的数据。线程间的工作内存也相互独立;

内存间交互操作

原则:以下操作都是原子的,不可再分的(double和long例外);

  • lock(锁定):作用主内存变量,把变量标识为一个线程独占状态;

  • unlock(解锁):作用主内存变量,把处于锁定状态的变量释放出来;

  • read(读取):作用主内存变量,把一个变量的值从主内存传输到线程的工作内存;

  • load(载入):作用主内存变量,把read操作从主内存得到的变量值放入工作内存副本中;


  • use(使用):作用于工作内存,把工作内存中的变量值传递给执行引擎,每当虚拟机遇到一个使用变量的值的字节码就会执行该操作;

  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行该操作;

  • store(存储):作用于工作内存变量,把工作内存中的一个变量的值传递给主内存中,以便随后的write操作使用;

  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;

变量从主内存拷贝到工作内存:按序执行read,load

变量从工作内存同步回主内存:按序执行storewrite

仅需按序执行,不是连续执行例如:

read a, read b, load b, load a;

规则

  1. 不允许readloadstorewrite单独出现;
  2. 不允许一个线程丢弃最近的assign操作;
  3. 不允许无assign操作的变量执行store
  4. 一个新变量只能在主内存诞生;
  5. 一个变量同一时刻只允许一条线程对其lock操作;同一线程多次lock需执行相同次数的unlock操作才能解锁;
  6. 当对一个变量执行lock操作,会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行loadassign操作初始化变量的值;
  7. 不允许unlock一个没有lock的变量,不允许unlock一个其他线程线程锁定的变量;
  8. 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行storewrite操作);

简化:Java内存模型的操作简化为,readwritelockunlock;只是语言描述上的简化,Java内存模型的基础设计并未改变;

volatile变量的特殊规则

作用:1. 保证此变量对所有线程的可见性(一条线程修改了变量值,其他线程可 以立即得知);2. 禁止指令重排序优化;

保证可见性:1. 运算结果不依赖变量的当前值,或者能够确保只有单一的线程修 改变量的值;2. 变量不需要与其他的状态变量共同参与不变约束;

class Test {
    volatile boolean flag;
    public void shutdown() {
        flag = true;
    }
    public void doSometings {
        while (!shutdown()) {
            // todo
        }
    }
}

禁止重排序: 有volatile修饰的变量赋值后,字节码多了一个lock add$0x0, (%esp)(空操作),该操作相当于内存屏障,重排序时不能将后面的指令重排序到内存屏障中前的位置。多的指令由于IA32规范中规定lock前缀不允许使用nop指令,该指令的作用是将本处理器缓存写入内存,该动作引起别的处理器或者别的内核无效化其缓存,这种操作相当于对缓存中的变量做了一次类似store和write操作,可以使volatile变量的修改对其他处理器立即可见。

java内存模型对volatile特殊规则:

  1. 线程对变量的执行流:load后面是use,线程对变量的use动作可认为是和线程对变量的read,load动作相关联且连续一起出现;

    在工作内存中,每次使用变量前都必须从主内存刷新最新的值,用于保证能看见其他线程对变量V所作的修改;

  2. 线程对变量的执行流:assign的后一个动作是store,线程对变量的assign动作可认为是对变量store,write相关联必须连续且一起出现;

    在工作内存中,每次修改变量后都必须立刻同步回主内存,用于保证其他线程可以看到自己对变量V所作的修改;

  3. 如果线程1变量A实施use或assign,再对变量B实施use或assign;那么对变量A实施read或write,再对变量B实施read或write;

    保证volatile修饰的变量不会对指令重排序优化,保证代码的执行顺序和程序的执行顺序相同;

long与double特殊规则

背景:Java内存模型对8中操作都具有原子性,但对于64位的数据类型可以由虚拟机自行选择是否保证load,store, read,write四个操作原子性。

现状:主流的64位JVM并不会出现非原子性访问行为,常用的32位HotSpot虚拟机,对long类型的数据存在非原子性访问的风险。在实际开发中,除非明确知道会发生线程竞争,一般不需要因为这个原因刻意的把long和double变量专门声明为volatile。

原子性

由Java内存模型保证的原子性变量操作包括 read,load, assign, use, store, write六个。

可见性

当一个线程修改了共享变量的值,其他线程能够立即得知此修改。Java内存模型是通过变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存保证的。只不过被volatile修饰的变量,新值能立即同步到主内存,以及立即从主内存刷新。synchronizedfinal都可实现可见性。

  • synchronized实现可见性

    由于unlock前必须先把此变量同步回主内存中;

  • final实现可见性

    被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么其他线程中就能看见final字段的值。

有序性

一条线程内,所有的操作都是有序的,若一个线程观察另一个线程,所有的操作都是无序的;

  • volatile保证线程之间操作有序性

    volatile本身包含了禁止指令重排序;

  • synchronized保证线程之间操作有序性

    一个变量同一时刻只允许一条线程对其进行lock操作;

先行发生原则

是判断数据是否存在竞争,线程是否安全的判断原则;

定义:Java内存模型中定义的两项操作之间的偏序关系,若A操作先于B发生,那么A操作的影响能被B观察到。以下是无需任何同步手段就能成立的先行规则。

  • 程序次序规则:在一个线程内,按照控制流顺序,先写先执行(控制流顺序,非程序代码顺序);
  • 管程锁定规则:一个unlock操作先于后面对同一个锁的lock操作;
  • volatile变量规则:对一个volatile变量写操作先发生于后面的读操作;
  • 线程启动规则:start()方法先于其他动作;
  • 线程终止规则:终止检测是线程的最后操作;
  • 线程中断规则:对线程interrupt()方法的调用先发生于被中断线程的代码检测到中断事件发生;
  • 对象终结规则:构造函数先发生于析构函数;
  • 传递性:若A操作->B操作, B->C那么A->C;

Java和线程

线程的实现

主要方式:内核线程实现(1:1实现),用户线程实现(1:N实现),使用用户线程加轻量级进程混合实现(N:M实现);

内核线程实现

内核线程(KLT),直接由OS内核支持的线程,每个线程可以视为内核的分身;程序一般通过内核线程的高级接口——轻量级进程(LWP)去使用内核线程。由于每个轻量级进程都由一个内核线程支持,因此只有先支持 内核线程,才能有轻量级进程(1:1)。

优势

  1. 每个LWP都是一个独立的调度单元,一个阻塞不会影响整个进程继续工作;

不足

  1. 基于内核线程实现,各种操作需要进行系统调用,代价高;
  2. 占用一定的内核资源;
  3. 一个系统支持的轻量级线程数量是有限的;
用户线程实现

用户线程实现的方式被称为1:N实现;用户线程是指完全建立在用户空间的线程库上,系统内核不会感知到用户线程的存在及如何实现。

JVM篇·JVM内存模型与线程_第1张图片

优势

  1. 不需要内核的支持;
  2. 无需切换到内核态,操作可以非常快速且低消耗;
  3. 能够支撑大规模的线程数量;

不足

  1. 无内核态的支持,所有的线程操作调度均需用户处理;
  2. 对于阻塞的处理,如何将线程映射到其他处理器上等问题实现困难;
  3. 使用用户线程实现的程序通常复杂;
混合实现

将内核线程与用户线程一起使用的实现方式(N:M);既存在用户线程,也存在轻量级进程。

优势:

  1. 用户线程建立在用户空间中,其创建管理代价小;
  2. 可支持大规模用户线程并发;
  3. 轻量级进程作为用户线程和内核线程之间的桥梁,可以使用内核提供的线程调度与处理器映射;
Java线程实现

主流的JVM线程模型普遍被替换成基于OS原生线程模型来实现,即采用1:1的线程模型;

Java线程调度

线程调度:系统为线程分配处理器使用权的过程;

调度方式:协同式线程调度,抢占式线程调度;

Java线程调度方式:抢占式线程调度;

协同式线程调度

概念:协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程主动通知系统切换另一个线程;

优势:实现简单;调度交由线程本身处理;一般可以避免同步问题;

缺陷:线程执行时间不可控,线程阻塞后无法交出处理器;

抢占式线程调度

概念:抢占式调度的多线程系统,由系统分配线程的执行时间,线程调度由系统处理;

优势:线程执行时间是系统可控,不会因为一个线程阻塞导致整个系统阻塞;

Java线程优先级不一定有效的原因

因为线程是是交给操作系统去管理,线程的优先级也是由操作系统上的原生线程来实现,最终的调度是由操作系统说了算的,不同系统的优先级不一样,且部分操作系统可自行改变优先级,因此不能通过优先级来判断线程的执行顺序;

Java线程的状态

java语言 定义了6种状态,一个线程只能有其中的一种状态;

  1. 新建(New):创建后尚未启动的线程;
  2. 运行(Runnable):包括操作系统线程状态种的Running和Ready,线程有可能在运行,也有可能等待操作系统分配执行时间;
  3. 无限期等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,他们要等待其他线程显示唤醒;以下方法会让线程进入此状态;
    1. 没有设置Timeout参数的wait()方法;
    2. 没有设置Timeout参数的join()方法;
    3. LockSupport::park()方法;
  4. 限期等待(Timed Waiting):处于这种状态的线程也不会分配处理器执行时间,系统将在一段时间后自动唤醒;以下方法可以让线程进入限期等待状态:
    1. Thread::sleep()方法;
    2. 设置Timeout参数的wait()方法;
    3. 设置Timeout参数的join()方法;
    4. LockSupport::parkNanos()方法;
    5. LockSupport::parkUntil()方法;
  5. 阻塞(Blocked):线程因锁被阻塞,将在另一个线程放弃这个锁的时候发生;在程序等待进入同步块时变成这种状态;
  6. 结束(Terminated):已终止线程的线程状态,线程已经结束执行;

JVM篇·JVM内存模型与线程_第2张图片

协程与纤程

协程:能够协同式调度的线程;

分类:有栈协程、无栈协程;

有栈协程:能够完整的做栈的保护,恢复工作;

无栈协程:无栈协程本质是一种有限状态机,状态保存在闭包种,是更轻量级的协程;例如某些语言的awaitasyncyield

java的纤程(有栈协程):可以使用 Quasar里的Fiber 去实现,在字节码层面将线程运行换成 Quasar调度器处理;

你可能感兴趣的:(JAVA,JVM,java,操作系统,多线程)