• JVM线程基本原理,你一定要知道!


    一、JAVA内存模型与线程

    1 CPU工作效率比IO工作效率大

    1.1 为什么
    计算机的存储设备与处理器的运算速度有几个数量级的差距

    1.2 怎么处理CPU与IO之间效率的差距
    加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)作为内存与处理器之间的缓冲

    内存->缓存(计算)->内存
    

    1.3 引发什么问题?
    缓存一致性,也是因为这一点,所以有了线程安全问题

    1.4 操作系统是如何解决缓存一致性的问题的?
    通过协议进行处理

    1.5 什么是指令重排序?
    处理器可能会对输入代码进行乱序执行(Out-of-Order Execution)优化,以让处理器内部的运算单元能尽量被充分利用
    在这里插入图片描述

    2 JAVA内存模型

    2.1 为什么需要使用JAVA自定义的内存模型
    屏蔽各种硬件和操作系统内存访问差异

    2.2 JAVA工作的主内存与工作内存是什么?
    主内存

    • 所有变量都存储在主内存
    • 对应堆中的对象实例数据部分
    • 直接对应于物理硬件的内存

    工作内存

    • 线程私有的内存区域,存储主内存中变量的副本
    • 对变量的操作都是在工作内存中,不能对主内存进行操作
    • 不同线程不能相互访问工作内存
    • 对应虚拟机栈中的部分区域
    • 对应物理硬件的寄存器和高速缓存
      在这里插入图片描述

    3 内存间交互操作指令

    lock(锁定)
    作用于主内存的变量,把一个变量标志为一条线程独占的状态

    unlock(解锁)
    作用于内存的变量,把一个处于锁定状态的变量释放出来

    read(读取)
    把变量的值从主内存传输到线程的工作内存

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

    use(使用)
    把工作内存中一个变量的值传递给执行引擎

    assign(赋值)
    从执行引擎接受到的值赋给工作内存的变量

    store(存储)
    把工作内存中的一个变量的值传送到主内存中

    write(写入)
    把store操作从工作内存中的到的变量对的值放入主内存变量中

    4 指令操作之间的规则

    不允许read和load、store和write操作之一单独出现。不允许一个变量从主内存读取了但工作内存不接受,或从工作内存发起会写但主内存不接受

    不允许丢弃assign操作,在工作内存中改变之后必须把该变化同步回内存

    不允许一个线程无原因的把数据从线程的工作内存同步回主内存

    一个新的变量只能再主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化的变量

    一个变量在同一时刻只允许一条线程对其lock操作,lock几次,需要unlock几次

    执行lock操作,会先清除工作内存中的值,然后重新获取主内存中的该值

    如果一个变量事先没有被lock操作,不允许unlock,不允许unlock其他线程lock的变量

    unlock的时候,需要把变量同步回主内存

    5 对于volatile型变量的特殊规则

    5.1 关键字volatile是最轻量级的同步机制

    5.2 volatile的两种特性

    5.2.1 保证变量对所有线程的可见性(更新某个变量值,新值对于其他线程来说是可以立即得知的),但不意味这样就是线程安全的,只有在以下两种场景,volatile才是线程安全的

    • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
    • 变量不需要与其他状态变量共同参与不变约束

    5.2.2 禁止指令重排序优化。保证变量赋值操作顺序与程序代码中的执行顺序一致,这一个也是volatile的特性,不仅仅是锁的特性

    5.3 volatile程序性能

    • 性能优于锁
    • 读操作与普通变量几乎没有什么差别
    • 写操作因为需要插入许多内存屏障指令保证不发生乱序,所以会慢一点

    5.4 volatile内存中的特殊规则

    • load、read的动作必须连续一起出现(每次使用变量的时候必须先从主内存中刷新最新的值)
    • assign、store的动作必须连续一起出现(每次修改完V后都必须立刻同步回主内存,用于保证其他线程可以看到自己对变量的修改)
    • 执行动作是有序的,保证代码的执行顺序与程序的顺序相同

    因为这些特殊规则才促就了volatile的特性

    5.5 对于long和double型变量的特殊规则
    因为虚拟机把long和double变量都实现成原子操作,所以一般不需要把用到的long和double变量专门声明为volatile

    5.6 并发中的三个特性

    5.6.1 原子性
    保证原子性变量操作,如果需要更大范围的原子性操作,那需要用锁或synchronized来保证

    5.6.2 可见性
    当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
    synchronized 和 final 能保证可见性

    5.6.3 有序性
    如果在本线程内观察,所有的操作都是有序的,如果一个线程中观察另一个线程,所有操作都是无序的
    volatile synchronized能保证有序性

    5.7 先行发生原则
    判断数据是否存在竞争、是否安全的主要依据

    5.7.1 是什么?
    定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等

    5.7.2 天然的先行发生关系(无须加锁)

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

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

    volatile变量规则。对一个volatilc变量的写操作先行发生于后面对这个变量的读操作。

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

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

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

    对象终结规则。一个对象的初始化完成先行发生于它的finalize()方法的开始。

    传递性。如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

    5.7.3 时间先后顺序与先行发生原则之间基本没有太大的关系,衡量并发安全问题时不要受到时间顺序的干扰,一切必须以先行发生原则为准

    6 Java与线程

    6.1 线程的实现

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

    主流操作系统都提供了线程的实现,Java线程的关键方法都是声明Native,所以是直接使用了平台相关的方法去创建线程

    6.2 实现线程的3种方式

    6.2.1 使用内核线程
    内核线程(Kernel-Level Thread KLT) 就是直接由操作系统内核支持的线程,这种线程由内核来完成线程的切换。

    内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。

    每个内核线程都可以视为内核的一个分身。

    支持多线程的内核叫做多线程内核。

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

    局限性

    • 基于内核线程实现,线程操作(创建,析构、同步)都需要系统调用,代价相对比较高,需在用户态(User Mode)和内核态(Kernel
      Mode)中来回切换
    • 需要消耗内核资源(如内核的栈空间)

    6.2.2 使用用户线程
    广义上面讲,一个线程只要不是内核线程,就可以认为是用户线程

    从定义上讲,轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作都要进行系统调用,效率会受到限制

    狭义上面讲,完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现

    用户线程的建立、同步、销户和调度完全在用户态中完成,不需要内核的帮助

    如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的

    这种进程与用户线程之间1:N的关系称为一对多的线程模型
    在这里插入图片描述

    优势:不需要内核支援

    劣势:没有系统内核的支援,所有的线程操作都需要用户程序自己处理

    • 线程的创建、切换和调度
    • 阻塞如何处理
    • 如何将线程映射到其他处理器上

    因为用户线程实现程序比较复杂,所以使用用户线程的程序越来越少

    6.2.3 使用用户线程加轻量级进程混合实现
    用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。

    轻量级进程作为桥梁,可以使用内核提供线程调度功能以及处理器映射功能,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。

    这种关系为N:M关系,多对多的线程模型
    在这里插入图片描述

    6.3 JAVA线程的实现

    • Windows版和Linux版本使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程中。
    • Solaris通过支持一对一、多对多的线程模式。

    6.4 JAVA线程调度
    6.4.1 协同式线程调度
    线程的执行时间由线程本身控制,线程把自己的工作执行完,主动通知系统切换到另外一个线程
    好处

    • 实现简单
    • 切换操作堆线程自己是可知的,所以没有什么线程同步问题

    坏处

    • 线程执行时间不可控制,如果一个线程编写有问题,那程序一直会阻塞在那里

    6.4.2 抢占式线程调度(JAVA线程实现方式)
    线程的切换不由线程本身来做决定

    好处

    • 执行时间是可控的,不会有一个线程导致整个进程阻塞

    使用优先级来建议系统对某个线程多分配执行时间

    • 提供了10个级别的线程优先级
    • 优先级是不太靠谱的,因为优先级不一定与系统线程优先级相对应,有可能有几个优先级相同的情况
    • 优先级可能会被系统自行改变,如在window系统中

    6.5 线程状态切换

    新建(new)
    创建后尚未启动的线程处于这种状态

    运行(Runable)
    Runnable包括了操作系统状态中的Running和Ready,也就是处于此线程由可能正在执行,也有可能正在等待CPU为他分配执行时间

    无限期等待(Waiting)
    处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显示的唤醒(释放锁)

    • Object.wait()
    • Thread.join()
    • LockSupport.park()

    限期等待(Timed Waiting)
    处于这种状态的线程不会被分配CPU执行时间,无须等待被其他线程显示的唤醒,在一定时间之后会被系统自动唤醒。

    阻塞(Blocked)
    与等待状态的区别是,在等待获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生。

    结束(Terminated)
    已终止线程的线程状态。
    在这里插入图片描述
    感谢你看到这里,我是程序员麦冬,一个java开发从业者,深耕行业六年了,每天都会分享java相关技术文章或行业资讯

    欢迎大家关注和转发文章,后期还有福利赠送!

  • 相关阅读:
    Android Wifi简单管理与操作
    Android 语音识别(其它资料里面的代码)
    Android 读取资源文件下面的文件
    slice,substr和substring的区别
    !important minheight
    Jquery之美中不足之三delegate的缺憾
    事件切片
    QWrap简介之:apps果实篇之:定制
    QWrap简介之:apps果实篇之:小结
    QWrap简介之:结语
  • 原文地址:https://www.cnblogs.com/hzcya1995/p/13290176.html
Copyright © 2020-2023  润新知