第12章 java内存模型与线程

315 阅读11分钟

第一节:java内存模型

  • jvm规范定义一种java内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异。以实现让java程序在不同平台下都能达到一致的内存访问效果。

一、主内存和工作内存

概述:

  • java内存模型的主要目标,是定义程序中各个变量的访问规则
  • 即在jvm中将变量存储到内存和从内存中读取变量的底层细节
  • 这里的变量不包括虚拟机栈和方法参数。

1、主内存:

  • 所有线程的公共内存
  • 所有变量都必须保存在主内存中。

2、工作内存

  • 线程独有,可与处理器高速缓存类比
  • 工作内存保存了该线程使用到的从主内存中变量的副本拷贝
  • 线程对变量的所有操作必须在自己的工作内存中,不能在主内存中
  • 不同线程中,不能直接访问对方的工作内存,变量值的传递只能通过主内存中转
  • 所有线程间的通信,都要通过主内存中转完成。
  • **Node:**内存模型和以前的运行时数据区(栈,堆,方法区等)并不是同一个层次的内存划分,没有必然关系

二、内存交互操作

  • 概述:主内存和工作内存之间的交互协议,定义了两者之间数据同步的细节处理(8种操作都具有原子性)。

1、交互协议的八种操作

  • lock(锁定):作用于主内存的变量,标识该变量被一个线程独占状态
  • unlick(解锁):作用于主内存的变量,把处于锁定的主内存的变量释放出来,允许其他线程锁定。
  • read(读取):作用于主内存的变量,把一个主内存的变量的值传输到线程的工作内存,以便后面的load使用(read的下一个操作必须是load,否则该操作不能是read,这两个操作成对出现)。
  • load(载入):作用于工作内存的变量,把read从主内存中得到的变量值放入到工作内存
  • use(使用):作用于工作内存的变量,它把工作内存的一个变量传递给执行引擎。当jvm遇到需要使用需要使用变量值的时候,就会执行这个操作(比如:操作数栈的内容弹栈进行数据计算)
  • assign(赋值):作用域工作内存的变量,把从执行引擎获取到的值赋值给工作内存变量。每当jvm遇到一个给变量赋值的字节码指令时执行这个操作。(比如:计算完毕后,结果压入操作数栈)
  • store(存储):作用于工作内存的变量,把工作内存的变量值传递到主内存中。(必然和后续的write成对出现,也就store的后一个操作必然是write)
  • write(写入):作用于主内存的变量,把从store获取到的变量值,放入到主内存中。

三、volatile型变量的特殊规则(针对多核CPU)

概述:

  • volatile是jvm提供的最轻量级的同步机制,但是并不是线程安全的
  • volatile修饰过得变量,对所有线程可见。即一条线程修改该变量,其他线程理解得知
    • 普通变量,线程A修改后回写入主内存,线程B主动去主内存读取才能得知该变量的新值。
    • volatile,线程A修改后回写入主内存,其余线程每次使用前,都必须主动去主内存读取新值(不加锁,一旦读取到操作数栈进行运算,并发情况下就不安全)

1、保证可见性:

  • 必须符合以下规则:
    • 运算结果并不依赖变量的当前值,或者能保证只有单一线程修改变量的值
    • 变量不需要与其他状态变量共同参与不变约束。

2、禁止重排序

  • A线程内部study()最后一行flag=false。线程B根据flag判断是否执行。
    • 因为重排序的原因,可能造成study()其他代码未执行完,flag被置为true,线程B开始执行
  • 原理:volatile修饰的变量,会增加一个内存屏障。当前修改变量的cpu把catche写入主内存的时候,会造成其他观测cpu的catche失效

3、选择volatile的意义:

  • 同步机制性能要优于锁,由于锁的消除和优化,很难量化的说明synchronized快多少(大多数情况是比锁开销低)。
  • volatile变量读操作和普通变量差不多,写操作会慢一些(有内存屏障)。

4、volatile对8种交互协议影响(线程T,两个被volatile修饰的变量V、W)

  • 线程T对变量V的执行动作必须是read、load、use这三者依次顺序。(这条规则保证每次使用V前,都必须先刷新主内存中变量最新值,保证能获取到其他线程对变量V的修改的最新的值)。
  • 线程T对变量V的执行动作必须是assign、store、write这三者依次顺序(这条规则要求,在工作内存中,每次修改完变量必须同步回主内存,保证其他线程可以看到V的变量的修改)。
  • 修改V和W两个变量,命令有交叉的情况,那么顺序执行(这条规则规定,被volatile修饰的变量不会发生指令重排序,代码的执行顺序和程序的执行顺序相同)

四、原子性、可见性和有序性

概述:jmm就是围绕并发过程中的原子性、可见性和有序性三个特征展开的

1、原子性:

  • 内存交互的6种(read、load、use、assign、store以及write)操作(long、double的非原子性协定)保证了基本数据类型操作的原子性。
  • 如果需要更大范围的原子性保证,jmm还提供了lock、unlock反应到java代码就是synchronized关键字

2、可见性:

  • 当一个线程修改了变量值,其他线程立即得知这个修改
  • volatile关键字保证了修改变量值立即同步回主内存,每次使用前读取主内存中变量的值。其他普通变量不保证可见性。
  • final关键字可见性。被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this传递出去(this没有发生引用逃逸),那在其他线程中就能看到final字段的值。
  • synchronized加锁,也能保证可见性

3、有序性:

  • 如果在单线程内部观察操作全部有序,如果一个线程中观察另一个线程那么操作就全部是无序的

五、先行发生原则

概述:

  • 操作A比操作B先发生,A产生的影响能被B观察到。

规则:

  • 程序次序规则:本线程内按照程序代码顺序执行。
  • 管程锁定规则:lock之前,变量必须要是unlock(同一个锁)
  • volatile变量规则:对一个volatile修饰的变量写操作先发生于对这个变量的读操作
  • 线程启动规则:Thread的start()现行发生于线程的其他动作
  • 线程终止规则:线程所有的操作,都要在终止之前完成
  • 线程中断规则:对线程的intercept()调用,先行发生于被中断线程的代码检测到中断事件发生。(先中断)
  • 对象终结规则:对象的初始化完成,先行发生于finalize()方法开始。
  • 传递性:A比B先发生,B比C先发生,那么A就比C先发生。

第二节:java与线程

一、线程的实现

概述:

  • 线程可以把一个进程的调度和资源分配分开。
  • 线程既可以共享进程内存(内存地址、文件IO等),又可以独立调度(CPU的最小调度单元)
  • 线程的实现有三种方式:内核线程实现、用户线程实现、用户线程和轻量级进程混合实现

1、内核线程实现

  • 由操作系统内核实现的线程,线程切换通过内核调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
    • 每个内核线程可以看做是内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核叫多线程内核。
  • 程序不会直接使用内核线程,而是通过使用内核的线程接口-轻量级进程。
    • 这个轻量级进程也就是我们说的线程
    • 每个轻量级进程都由一个内核线程支持,因此内核要支持内核线程才能支持轻量级进程,他们一对一关系
  • 劣势:
    • 基于内核线程实现,因此线程操作(创建、析构、同步)都需要系统调度。系统调度要在用户态和内核态来回切换,调度成本高
    • 每个轻量级进程都要有一个内核线程支持,因此轻量级进程需要消耗一定的内核资源(内核线程栈空间)。系统支持的轻量级进程是有限的

2、用户线程实现

  • 用户线程完全建立在用户空间的线程库上。内核空间不能感知线程存在的实现。
    • 用户线程的建立、同步、调度和销毁完全在用户态下完成(快速低消耗),无需内核线程
    • 无需在用户态和内核态下切换,性能更高。
    • 支持更大规模的线程数量,部分高性能数据库采用一对多的模型设计
  • 劣势:
    • 线程的操作(创建、切换、调度)都要用户自己实现,且异常复杂
    • 由于操作系统把处理器资源分配到进程,那阻塞如何处理、多处理器如何将线程映射到其他处理器上等问题会变的异常困难,甚至无法完成

3、用户线程+轻量级进程

  • 用户线程和轻量级进程,共同存在,支持多对多
  • 用户线程任然全部建在用户空间
  • 轻量级进程作为内核线程和用户线程之间的桥梁

4、java线程

  • 根据不同操作系统,实现不同
  • 在windows和Linux下,使用的轻量级进程,即轻量级进程和内核线程是一对一的关系。

二、java线程调度

概述:系统为线程分配处理器使用权的过程

  • 协同式线程调度
  • 抢占式线程调度

1、协同式调度

  • 线程的执行时间由线程本身控制
    • 线程执行完毕后,通知处理器切换线程
    • 实现简单,不存在同步问题
  • 劣势:
    • 执行时间不可控,如果线程不释放内核,就会造成程序阻塞

2、抢占式调度

  • 线程执行时间由内核随机调度,线程切换不由线程本身决定
    • java中的Thread.yield(),可以让出线程执行,但是线程无法主动获取时间
    • java使用的就是抢占式调度
    • 不会因为一个线程阻塞,造成软件阻塞
  • 可以通过设置线程优先级,建议系统给线程分配时间
    • java线程映射到内线程实现,因此这种建议不一定生效
    • 系统可能会修改优先级

三、线程状态切换

1、新建:

  • 创建线程后未启动状态

2、运行:

  • Runable包含了内核线程的状态的running和ready,此状态的线程可能在执行也可能在等待CPU为其分配时间

3、无线等待(Waiting):

  • 处于此种状态的线程不会被CPU分配时间,必须由其他线程显示的唤醒。以下方法会让线程进入无线等待:
  • 没有设置Timeout参数的Object.wait()
  • 没有设置Timeout参数的Thread.join()
  • LockSupport.park()

4、限期等待:

  • 处于这种状态的线程CPU不会分配时间,但是也无需其他线程唤醒。一定时间后由系统自动唤醒。
  • Thread.sleep()
  • 设置了Timeout参数的Object.wait()
  • 设置了Timeout参数的Thread.join()
  • LockSupport.parkNanos()
  • LockSupport.parkUntil()

5、阻塞(Blocked):

  • 线程被阻塞
  • 线程阻塞:线程等待获取一个排他锁,这个事件在另外一个线程放弃这个锁的时候发生
  • 等待状态:等待一段时间,或者被其他线程唤醒。(线程进入安全区)

6、结束:

  • 已经终止的线程状态,线程结束