【JVM】JMM 内存模型

发布于:2024-05-04 ⋅ 阅读:(35) ⋅ 点赞:(0)

JMM

概述

内存模型

  1. java[内存模型](Java Memory Model) 和 [内存结构]
  2. JMM规定了在多线程下对共享数据的读写时,对数据的原子性 有序性 可见性的规则和保障。

原子性

  1. 原子性问题:

在这里插入图片描述

i++和i–不是原子性操作! 所以一个i++指令会在执行过程中被另一个线程执行!
在这里插入图片描述

  1. 问题分析:

在这里插入图片描述

(1)共享的变量信息是放在主内存中的,线程呢是在工作内存中
多线程情况下指令交错产生的问题:
在这里插入图片描述

  1. 问题解决
    java中通过synchronized来保证了原子性

在这里插入图片描述

EntryList: 排队等候区:阻塞住

Owner: 有一个一个线程可以进入成为owner

monitorExit: 表示owner中线程执行完毕,会通知EntryList中等待的线程。然后就可以拥有抢夺owner的机会,当等待的线程成为owner后就会执行monitorenter 锁住monitor区域

WaitSet: 是线程wait() notify()的时候需要用到的区域

在这里插入图片描述

优化:
在这里插入图片描述

Java锁机制

64位开启指针压缩为例:

在这里插入图片描述

  1. java中锁的实现:每个对象都拥有一把锁,该锁存放在对象头中。锁中记录了当前对象被哪个线程所占用。

在这里插入图片描述

在这里插入图片描述

  1. 对象的结构

    1. 对象头:(存放了一些对象本身的运行时信息)
      1. Mark Word:存储了很多和当前对象运行时状态有关的数据。
      2. Class Pointer: 指向了堆中当前类class对象。
    2. 实例数据:属性 + 方法
    3. 对齐填充字节: 为了满足对象的大小为8个字节的倍数,无实际意义。
  2. 锁状态:对象头的Mark Word中"锁标志位"分别对应四种锁状态:无锁、偏向锁、轻量级锁、重量级锁

  3. synchronized(重量级锁)实现线程同步:

    • 通过生成的monitorenter和monitorexit来实现同步。结合monitor区域来实现线程同步。

synchronized 缺点:本质:通过线程状态的切换。用户态<–>内核态,所以很慢。

synchronized 编译后是通过jvm指令monitorenter和monitorexit来实现同步,而该jvm指令实际上依赖操作系统的mutex lock来实现的。java线程实际上是对操作系统线程的映射,所以每当挂起/唤醒一个线程,都要从"用户态"切换。操作系统的"内核态",这种操作是比较重量级的!一些情况下甚至切换时间本身会超过线程执行任务的时间!所以使用synchronized会对性能产生严重的影响!

jdk 6后,对synchronized进行了优化,引入了无锁、偏向锁、轻量级锁

  • 对应了对象头Mark Word中的四种状态[无锁 偏向锁 轻量级锁 重量级锁]

注意锁只能升级不能降级!

synchronized 优化

synchronized是如何优化的?四种状态如何变化的?

  1. 无锁:线程都可以来访问该资源

    1. 无竞争
    2. 存在竞争.可以通过非锁方式,同步线程。失败重试(乐观锁思想)
      • CAS: Compare And Swap:CAS在操作系统中通过一条指令来实现,所以能够保证原子性!
        且在标价和交换的过程中,必须是原子性。不然两个线程就会抢到一个资源!
      • cas和锁的区别:
        • 锁: 通过锁定资源的方式,保证线程同步。给对象加锁。
        • cas: 通过别的方式,不锁定资源。
  2. 偏向锁:

    1. 对象能够认识这个线程,这个线程来了直接把锁交出去,认为对象偏向这个线程。
    2. 在偏向锁状态时通过线程ID来确定当前想要获得对象锁的线程。
    3. 当有多个线程来竞争这把锁,偏向锁会升级为轻量级锁。
    4. 使用场景:只有一个线程来获取资源,单线程。
  3. 轻量级锁:这时线程会在虚拟机栈中开辟一块被称为Lock Record的空间,线程通过cas获取锁,一旦获取到锁,会复制该对象的mark Word,并且将Lock Record中的owner指针指向该对象,该对象的对象头的Mark Word的前30个bit也会指向栈中锁记录的指针,这就实现了对象与线程的绑定。

在这里插入图片描述

这时候也有其他线程想要获取该对象该怎么办?

  • 自旋等待:线程自己在不断的轮询循环,尝试着看一下目标对象的锁有没有被释放。
  • 这种方式区别于被操作系统挂起,因为对象的锁很快被释放的话,自旋就不需要系统中断和现场恢复。所以效率高一些。
  • 自旋相当于cpu在空转,如果时间过长会占用资源。
  • 如果自旋等待的线程数超过1个,那么轻量级锁会升级为重量级锁。
  1. 如果对象头中锁状态被标记为重量级锁,那么需要通过monitor来对线程进行控制。

    此时将会完全锁定资源,此时的管控也最为严格.

在这里插入图片描述

轻量级锁和重量级锁的区别:

  1. 重量级锁:通过monitor来控制线程同步,等待的线程会进入阻塞状态。涉及用户态到内核态的转变,这属于比较重量级的操作。
  2. 轻量级锁:通过等待的线程进行自旋的方式进行等待,不会阻塞。

CAS

  1. 不锁定资源,也能同步线程
  2. 预期原值与新值
    • 预期原值: 之前读到的资源对象的状态值
    • 新值: 想要将预期原值更新后的值
    • 内存位置: 资源本身当前的值(资源的状态值)
  3. 比较
    • 比较预期原值和内存位置(资源的状态值),一致的话将内存位置改为新值。这样别的线程的预期原值和内存位置就不一样了,就不操作了。
  4. 核心问题
    • "比较数值,并进行值的更新"这个操作必须是原子的!不然会出问题!!会有两个线程同时抢到…同时只能有一条线程进行操作!!!
  5. 如何实现CAS的原子性?
    • CAS在操作系统中就是一条指令,所以是原子性的。

CAS: 乐观锁。本质是一种无锁的同步机制。Java底层是通过Unsafe的方法实现CAS操作。

可见性

volatile: 一个线程写,多个线程读的场景,无法保证原子性。

一. 可见性问题

在这里插入图片描述

二. 可见性问题分析

线程t1对于t2修改后的值不清楚的原因是,t1线程频繁的读取boolean这个变量。然后即时编译器就会视为热点代码。将boolean的值缓存到高速缓存中。所以t1每次读取都是从自己的工作内存中读取。主内存中改了值,其实t1线程是感知不到的。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

synchronized 和 volatile 比较

  • synchronized 关键字用于给代码块上锁,volatile用于修饰成员变量/静态变量
  • synchronized 既可以保证原子性,也可以保证可见性。但是属于重量级的操作,是一种重量级锁,线程的用户态->内核态的转变。
  • volatile可以保证可见性和有序性,但是不能保证原子性。通过读写屏障保证线程之间的
    可见性和禁止编译器/处理器对指令进行指令重排序!

有序性

volatile保证了禁止指令重排序。

有序性的理解

  1. 单线程情况下jvm对其指令重排是不太影响结果的。(指令重排是一种优化)
  2. 多线程下指令重排就会产生一些问题!

单例模式下双重锁机制的变量需要加volatile,否则会指令重排。使得返回的对象不完整

在这里插入图片描述

在这里插入图片描述

happens-before

规定了哪些写操作可以对其他线程的读操作可见,是可见性与有序性的一套规则:

在这里插入图片描述

在这里插入图片描述

CAS

一. 概述

在这里插入图片描述

CAS是与volatile配合使用的一项技术。体现的是一种乐观锁的思想,是一种无锁并发技术。

compareAndSwap:如果旧值和共享变量相同,则进行swap,把共享变量改为结果。

CAS适用场景

  1. 竞争不激烈,多核CPU的情况下。因为等待的线程并不是进入阻塞状态,而是一种在尝试尝试。其他线程也需要占用CPU资源。如果竞争激烈,会影响效率!
  2. 获取"主内存中的值时",为了保证变量的可见性,需要使用volatile来修饰!结合CAS和volatile可以实现无锁并发!
  3. synchronized的比较:
    • 该种并发技术并不会使等待的线程进入阻塞状态,而是通过不断尝试的方式来操作。但是当竞争激烈时,等待的线程会占用过多CPU资源,导致效率下降!
    • CAS操作底层依赖于一个Unsafe类直接调用操作系统底层的CAS指令。

二. 底层实现

CAS操作底层依赖于一个Unsafe类直接调用操作系统底层的CAS指令。

三. 原子操作类

  • java中的悲观锁:synchronized
  • java中的乐观锁:CAS

在这里插入图片描述

在这里插入图片描述

锁膨胀

在这里插入图片描述

自旋锁:自旋是一种在重量级锁上的优化,并不会让等待的线程进入阻塞状态,而是处于一种
自旋重试的操作!

在这里插入图片描述

其他优化

  1. 缩少锁的粒度:
    1. ConcurrentHashMap: 原来HashTable是锁住了整个数组,ConcurrentHashMap是给数组(每个链表头)进行加锁,也就是分段的机制,当前段的上锁,不影响其他段的读写操作!

JMM总结

在这里插入图片描述

  1. Java中锁的实现

每个对象都拥有一把锁,该锁存放在对象头中。锁中记录了当前对象被哪个线程所占用。

  1. 对象的结构
  • 对象头:存放了一些对象本身的运行时信息,包括两部分:
    • Mark Word: 存储了很多和当前对象运行时状态有关的数据。
    • Class Pointer: 指向了堆中当前类class对象。
  • 实例数据: 属性 + 方法
  • 对齐填充字节: 为了满足对象的大小为8个字节的倍数,无实际意义。
  1. 锁状态

对象头的Mark Word中"锁标志位"分别对应四种锁状态:

无锁01、偏向锁01、轻量级锁00、重量级锁10

  1. synchronized (重量级锁) 实现线程同步

通过生成的monitorentermonitorexit来实现同步。

synchronized的缺点:本质上是通过线程状态的切换,用户态<–>内核态,所以很慢。synchronized编译后是通过JVM指令monitorentermonitorexit来实现同步,而这些JVM指令实际上依赖操作系统的mutex lock来实现的。Java线程实际上是对操作系统线程的映射,所以每当挂起/唤醒一个线程,都要从"用户态"切换操作系统的"内核态",这种操作是比较重量级的!一些情况下甚至切换时间本身会超过线程执行任务的时间!所以使用synchronized会对性能产生严重的影响!

monitor区域…

在这里插入图片描述

从JDK 6后,对synchronized进行了优化,引入了无锁,偏向锁,轻量级锁。

注意,锁只能升级不能降级!

synchronized 优化

synchronized是如何优化的?四种状态如何变化的?

  1. 无锁:线程都可以来访问该资源

    1. 无竞争

    2. 存在竞争.可以通过非锁方式,同步线程。失败重试(乐观锁思想)

      • CAS: Compare And Swap:CAS在操作系统中通过一条指令来实现,所以能够保证原子性!
        且在标价和交换的过程中,必须是原子性。不然两个线程就会抢到一个资源!

      • cas和锁的区别:

        • 锁: 通过锁定资源的方式,保证线程同步。给对象加锁。
        • cas: 通过乐观锁思想方式,不锁定资源。
  2. 偏向锁

    • 对象能够认识这个线程,这个线程来了直接把锁交出去,认为对象偏向这个线程。称为偏向锁。
    • 在偏向锁状态时通过线程ID来确定当前想要获得对象锁的线程。
    • 当有多个线程来竞争这把锁,偏向锁会升级为轻量级锁。
    • 使用场景: 只有一个线程来获取资源。单线程。
  3. 轻量级锁在这里插入图片描述

    • 这时线程会在虚拟机栈中开辟一块被称为Lock Record的空间。
    • 线程通过CAS获取锁,一旦获取到锁,会复制该对象的Mark Word,并且将Lock Record中的owner指针指向该对象,该对象的对象头的Mark Word的前30个bit也会指向栈中锁记录的指针,这就实现了对象与线程的绑定。
    • 这时候也有其他线程想要获取该对象该怎么办?
      • 自旋等待: 线程自己在不断的轮询循环,尝试着看一下目标对象的锁有没有被释放。
      • 如果自旋等待的线程数超过1个,那么轻量级锁会升级为重量级锁。
      • 锁膨胀: 竞争的线程想要通过CAS操作获取共享对象的锁的时候,获取不到。就会进行锁膨胀,转化为重量级锁!
  4. 重量级锁

    • 如果对象头中锁状态被标记为重量级锁,那么需要通过monitor来对线程进行控制…
      • 此时将会完全锁定资源,此时的管控也最为严格。

轻量级锁和重量级锁的区别:

  1. 重量级锁:通过monitor来控制线程同步,等待的线程会进入阻塞状态。涉及用户态到内核态的转变,这属于比较重量级的操作。
  2. 轻量级锁:通过等待的线程进行自旋的方式进行等待,不会阻塞。

CAS

  1. 不锁定资源,也能同步线程
  2. 预期原值与新值
    • 预期原值: 之前读到的资源对象的状态值
    • 新值: 想要将预期原值更新后的值
    • 内存位置: 资源本身当前的值(资源的状态值)
  3. 比较
    • 比较预期原值和内存位置(资源的状态值),一致的话将内存位置改为新值。这样别的线程的预期原值和内存位置就不一样了,就不操作了。
  4. 核心问题
    • "比较数值,并进行值的更新"这个操作必须是原子的!不然会出问题!会有两个线程同时抢到。同时只能有一条线程进行操作!
  5. 如何实现CAS的原子性?
    • CAS在操作系统中就是一条指令,所以是原子性的。

CAS: 乐观锁。本质是一种无锁的同步机制 + volatile。Java底层是通过Unsafe的方法实现CAS操作。


网站公告

今日签到

点亮在社区的每一天
去签到