Skip to content

JMM:java内存模型

jvm/jmm: 前者是jvm内存模型;后者是java内存模型 背景: 由于处理器的运算速度与存储设备的运算速度之差距引出了: 基于高速缓存的存储交互很好的解决了处理器和内存的速度矛盾, 再引出了:但是为计算器引入了一个新的问题:缓存一致性(每个处理器都有自己的的高速缓存,但是又公用同一主内存) 为了解决上述问题,提出内存模型 (多个处理器 -> 高速缓存) ->(一致性协议) -> (主内存)

作用: 内存模型(JMM):用来屏蔽java对各种硬件和操作系统的内存访问差异,以实现java程序在各个平台下都能达到一致性的内存访问效果 主要目标:定义程序中的各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节(需要注意的是这里的变量:包含着实例字段。静态字段,构成数组对象的元素,不包含局部变量,因为局部变量是线程私有,不存在共享,不存在竞争问题)

内存模型的相关的操作和规则

  1. 通过规定变量的使用,通过线程,主内存,工作内存三者之间的交互来使用变量
    1. 所有变量都在主内存
    2. 线程对变量的操作必须在工作内存中进行,不能直接读写主内存
    3. 不同线程之间无法访问对方的工作内存中的变量
  2. java线程 <-> 工作内存 <-> sava/load操作 <-> 主内存
  3. 主内存与工作内存通过以下8种方式进行交互,以下每种操作都是原子并不可再次分割的
    1. lock:工作于主内存,将主内存的变量加锁独占
    2. read:工作主内存,将主内存数据读取到线程的工作内存
    3. load:作用域工作内存:将read出来的值放置到工作内存的变量副本
    4. use:用于工作内存:将工作内存值使用,(传给执行引擎)
    5. assign:赋值:用于工作内存:将引擎接受的值赋予给工作内存变量->修改工作内存数据
    6. store:用于工作内存:将工作内存数据传给主内存
    7. write:主内存中:将store中的工作内存数据存的变量放到主内存变量中
    8. unlock:用于主内存:将锁释放,后续其他线程可再次锁定
  4. 对于以上8个操作有以下几点规定
    1. read/load。store/write :不能单独出现,必须一起说明取出来的值必须存在工作内存中,或者工作内存的数据更新后必须更新到主内存中
    2. assign使用完必须更新到主内存,后续需要store和write
    3. 一个新的变量必须在主内存诞生,所有工作内存的变量来自于主内存的副本
    4. “可重入锁”:可以多次lock但必须多次unlock且需要同一线程
    5. 如果没有lock则不能unlock
    6. 之行unlock之前必须将数据同步完到主存中
  5. volatile 关键字
    1. jvm提供的最轻量级的同步机制
    2. voatile只可以保证可见性,不能保证原子性(符合以下两种情况可以用volatile)
      1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
      2. 变量不需要与其他的状态变量共同参与不变约束
    3. 第二个语义(禁止指令重拍
      1. (双检锁)
      2. 通过在volatile之前之后加上store和load相关内存屏障指令操作保证处理器不发生乱序
      3. 理解:其实禁止指令重排序也是基于内存指令屏障指令实现的,类似双简单锁,编译后有(lock addl $ 0x0,(%esp))这个锁意味着我会将所有的修改同步到主存中,让你感觉我没有发生重排序:(指令重排序无法越过内存屏障

模型的特征

  1. 原子:8个内存屏障命令虚拟机:(lock/unlock):提供了更高层次的字节码monitorenter/monitorexit 通过了synchronized释放给用户
  2. 可见:只一个线程修改了变量可以立即被其他线程得知,三个关键字(volatile,final,synchronized)
  3. 有序:表现在在线程内部有序,在外部线程看该线程,该线程是混乱状态
    1. 可以通过volatile/synchronized实现有序
    2. happens-before(先行发生原则)
      1. 4个线程
        1. 程序内有序
        2. thread的start先于内部发生
        3. thread的join终于内部发生
        4. 对线程中断的判断,在中断之后interupt
      2. 一个volatile/一个锁
        1. volatile写操作先于发生后面对这个变量的读操作
        2. unlock操作先于发生于后面对于同一个锁的lock操作
      3. 一个构造方法
        1. 对象的初始化先于finalize操作
      4. 一个传递
        1. a先于b,b先于c,那么a先于c

java 与 线程

这里不是讲线程的创建,而是什么是线程这个东西

  1. 线程是比进程更轻量级的执行单位,线程的引入,可以把一个进程的资源分配和调度分开,各个线程可以共享进程的资源(IO/,内存地址等);线程又可以独立的调度,cpu即为调度的基本单位
  2. 几个概念
    1. KLT:内核线程(kernel-Level thread)
    2. LWP:轻量级进程:内核线程的一种高级接口,我们通常意义上讲的线程
    3. UT:用户线程 user Tread
  3. 线程实现的主要有三种方式
    1. 使用内核线程的是实现
      1. 直接由操作系统内核来支撑的线程,这种线程由内核实现线程的切换,每个内核线程视为内核的一个分身
      2. 用户线程不会直接使用内核线程,所以就通过他的接口LWP创建一个轻量级线程使用
      3. 其中轻量级线程和内核线程是一一对应关系
      4. 他的局限性在于:基于内核线程实现的,线程的创建析构同步需要进行系统调用,代价成本较高
    2. 使用用户线程的实现
      1. 这里首先指出:指的是狭义方面的,完全建立在用户空间的线程库,线程的创建吗同步调用在用户态完成,操作快速低消耗
      2. 劣势:由于没有系统内核的支撑,所有调度阻塞都要自己实现,这是因为cpu只给资源分给进程,至于具体你再怎么分给线程就是要处理的困难问题
    3. 使用用户线程 + 轻量级进程混合实现
      1. 用户线程的创建在用户空间中,所以线程的创建和切换等操作还是快速并廉价的
      2. 操作系统提供的接口即轻量级进程即为用户线程与内核线程之间桥梁
      3. 构造图即为:用户线程ut与lwp的对应关系是n:m 多对多的关系
        1. lwp与klt的关系是一对一的关系
  4. 线程间的调度方式
    1. 协同式调度
      1. 关键区别:执行时间通过线程本身来控制
      2. 优点在于实现简单,自己线程执行完成之后才会由其他线程来玩
      3. 缺点就是如果有异常就一直阻塞,别的线程也无法运行,导致整体阻塞
    2. 强占式调度
      1. 关键区别:执行时间由操作系统掌握
      2. 这里又个点:
        1. 我们可以建议某些线程多一些执行时间,通过java设置的10个优先级;thread.priority(·1-·10),越高优先级越高
        2. 但是这里还是看具体操作系统,像solaris有2^32次方的优先级/ window有7种优先级
        3. 对于window我们java对应关系就是多个java优先级对应同一个win的优先级
        4. 这种东西只是建议,win会有优先级推进器等功能,如果你这个线程经常性的快速执行,就不考虑你设置的优先级,优先让你执行
  5. java定义的5种线程状态
    1. 新建new:
    2. runnable:等待cpu调度
    3. waiting:wait / join / locksupport.park
    4. timedwaiting:wait(time), sleep(time),locksupport.parnanos
    5. block:等待获取锁
    6. terminated

基于 VitePress 构建