Java虚拟机学习笔记(五)——高效并发

版权声明:本文为博主原创,未经博主允许不得转载。 https://blog.csdn.net/weixin_36904568/article/details/90301235

一:内存与线程

1. Java内存模型的概念

定义程序中各个变量(实例字段、静态字段、数组元素)的访问规则

  • 所有变量存储在主内存
  • 每条线程还有自己的工作内存,保存了使用到的变量在主内存的副本拷贝。(不同线程之间无法访问彼此的变量,线程也无法访问主内存的变量)

2. Java内存模型之间的交互

(1)主要操作

  • lock锁定(主内存):把主内存的变量标志为一条线程独占的状态
  • unlock解锁(主内存):主内存的变量可被其他线程锁定
  • read读取(主内存):把变量值从主内存传输到线程的工作内存中
  • load载入(工作内存):把读取的变量值放入工作内存的副本中
  • use使用(工作内存):把工作内存的变量值传递给执行引擎
  • assign赋值(工作内存):把执行引擎得到的变量值传递给工作内存的变量
  • store存储(工作内存):把变量值从线程的工作内存传输到主内存中
  • write写入(主内存):把得到的变量值放入主内存的变量中

(2)过程

  • 主内存→工作内存:read+load
  • 工作内存→主内存:store+write
  • 使用变量:lock+unlock

(3)一般的规则

  1. 必须读写:不允许变量从主内存读取了,工作内存不接受。或者变量从工作内存发起回写了,主内存不接受
  2. 变化同步:工作变量被赋值后,必须同步回主内存。一个线程没有赋值操作,不能把数据同步回主内存
  3. 先初始化:数据需要先从主内存载入和赋值,才可以使用和存储
  4. 唯一加锁:一个变量在同一时刻只能被一条线程加锁,可以加多次锁
  5. 锁即清空:在加锁前需要先执行载入或赋值操作,初始化变量
  6. 自己解锁:只能解锁被自己锁住的变量
  7. 解锁同步:在解锁前需要先同步回主内存

(4)Volatile的规则

特点
1:保证变量对所有线程的可见性,无需通过主内存完成传递

无需加锁:

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

需要加锁:

  • Volatile变量在各个工作线程中不存在一致性问题,但是Volatile变量的运算在并发下依然存在一致性问题
2:禁止指令重排序优化

避免变量被重排序优化后提前执行

规则
  1. 先刷新(load+use):在工作内存中,每次使用volatile变量需要先在主内存刷新,保证得到最新值
  2. 快同步(assign+store):在工作内存中,每次修改volatile变量需要立刻同步回到主内存,保证主内存的变量是最新值
  3. 同顺序:被volatile修饰的变量,代码的执行顺序与程序的顺序相同

(5)long和double的非原子协定

允许虚拟机将没有被volatile修饰的64位数据的读写操作分为两次32位的操作进行,不保证read、load、store、write的原子性

3. Java内存模型的特征

(1)原子性

  • Java内存模型保证了read、load、assign、use、store、write操作的原子性
  • 通过lock和unlock获得更大范围的原子性(synchronized同步块)

(2)可见性

变量修改后同步写回主内存,变量读取前从主内存刷新变量值。

  • volatile变量会保证立即同步和立即刷新
  • synchronized同步块
  • final关键字

(3)有序性

  • 本线程的操作有序:线程内表现为串行
  • 其他线程的操作无序:指令重排序现象、工作内存与主内存同步延迟现象
    • volatile变量禁止重排序
    • synchronized同步块保证同一时刻只允许一条线程加锁

4. 先行发生原则

如果操作A先行发生于操作B,则操作A产生的影响能被操作B观察到。时间的先后顺序与先行发生原则没有必然联系

  • 同一线程之间
    • 程序次序原则:线程内按照控制流的顺序,前面的操作先行发生于后面的操作
  • synchronized同步块内
    • 管程锁定原则:一个unlock操作先行发生于同一个锁的lock操作
  • volatile变量
    • volatile变量原则:对一个volatile变量的写操作先行发生于读操作
  • 线程
    • 线程启动原则:Thread对象的start()方法先行发生于线程内的操作
    • 线程终止原则:线程内的操作都先行发生于对此线程的终止检测
    • 线程中断原则:对线程的interrup()方法先行发生于线程代码内对此线程的中断事件的检测
  • 对象
    • 对象终止原则:一个对象的初始化完成先行发生于finalize()
  • 传递性原则

5. Java与线程

(1)线程的实现

  • 内核实现(1:1):直接由操作系统的内核支持的线程,内核完成线程的切换和调度。程序通常使用轻量级进程(一个轻量级进程由一个内核线程支持)
    • 轻量级进程是一个独立的调度单元
    • 系统调用代价高
    • 内核资源消耗大
  • 用户线程实现(1:N):线程的创建、切换、调度和同步由用户自己完成
    • 操作快速
    • 资源消耗小
    • 程序过于复杂
  • 用户线程+轻量级进程实现(N:M)
    • 用户线程的创建、切换依旧由用户完成
    • 轻量级进程作为桥梁,使用内核提供的线程调度和处理器映射功能

Java在JDK1.2之前使用基于“绿色线程”的用户线程实现,在之后使用基于操作系统原生线程模型实现。

(2)线程的调度

协同式

线程的执行时间由线程本身控制,线程工作后主动通知系统切换其他线程

  • 实现简单
  • 无法控制线程执行时间
抢占式(JAVA)

线程的执行时间由系统分配,由系统调度线程。

  • 通过设置优先级分配时间
  • 不同平台的优先级不一致,优先级可能被系统改变

(2)线程的状态

  • 新建 New:创建完成,还没有启动
  • 运行 Runable:就绪或正在执行
  • 无限等待 Waiting:等待被其他线程唤醒
    • Object.wait()
    • Thread.join()
    • LockSupport.park()
  • 限期等待 Timed Waiting:等待一段时间后自动唤醒
    • Thread.sleep()
    • Thread.join(x)
    • LockSupport.parkNanos()
    • LockSupport.parkUtil()
  • 阻塞 Blocked:等待获取排他锁
  • 结束 Terminated:线程结束执行

二:线程安全

1. 线程安全的概念

(1)定义

当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要额外的同步,或者在调用方进行任何其他的协调操作,单次调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。

(2)Java中的线程安全

  • 不可变对象:一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要额外的线程安全保障措施
    • 基本数据类型:final
    • 对象类型:对象的任何行为都不会改变自己的状态
  • 绝对线程安全对象:完全满足线程安全的定义,调用对象不需要额外的同步手段
  • 相对线程安全对象:对该对象的单次操作时线程安全的,如果是连续调用,则需要额外的同步手段
  • 线程兼容对象:对象本身不线程安全,可以通过在调用端使用同步手段保证对象线程安全
  • 线程对立对象:无论是否采取同步手段,都无法保证对象线程安全

2. 线程安全的实现

(1)互斥+同步

定义

同步:多个线程并发访问共享数据,共享数据在同一时刻只被一条或一部分线程使用
互斥:实现同步的一种方法

特点
  • 悲观并发策略
  • 重量级,系统调用的开销大
方法
synchronized关键字

形成monitorenter和monitorexit字节码,需要一个reference类型的参数指明锁定和解锁的对象

  • synchronized指定对象:该对象作为reference类型的对象
  • synchronized未指定:修饰方法的实例对象或Class对象作为reference类型的对象
过程:
  • 在执行monitorenter指令时,获取对象的锁
    • 如果对象没有锁定,或已经被该线程拥有(可重入),则锁的计数器+1
    • 否则,阻塞等待
  • 在执行 monitorexit指令时,锁的计数器-1
  • 锁的计数器=0,释放对象的锁
java.util.concurrent的重入锁ReentrantLock
  • 等待可中断:持有锁的线程长期不释放锁的时候,正在等待的线程放弃等待,改为处理其他事情
  • 可实现公平锁:多个线程等待时,按先来先服务获取锁
  • 锁可以绑定多个条件

(2)非阻塞+同步

先进行操作,如果没有其他线程使用共享数据,操作成功。否则,再进行补偿措施(重试)

特点
  • 基于冲突检测的乐观并发策略
  • 需要保证操作和冲突检测具备原子性(硬件保证通过一条指令完成多次操作)
    • 测试并设置(Test-and-Set)
    • 获取并增加(Fetch-and-Increment)
    • 交换(Swap)
    • 比较和交换(Compare-and-Swap)
    • 加载链接(Load-Linked)
    • 条件存储(Store-Conditional)
方法

通过J.U.C包里面的原子类或者使用反射获取Unsafe.getUnsafe()使用

(3)无同步方案

如果一个方法不涉及共享数据,无需使用同步措施

  • 可重入代码:不依赖存储在堆的数据和公共资源、不调用非可重入方法、状态由参数传人
  • 线程本地存储:一个线程保存共享数据的操作过程

3. 锁优化

(1)自旋锁

多个处理器可以让多个线程并行执行,让后面请求锁的线程等待(执行一个忙循环),如果等待一段时间后没有成功获得锁,则挂起线程

  • 避免线程切换的开销
  • 消耗处理器资源
自适应的自旋锁

线程等待的时间由上一次在该锁上的自旋时间以及锁的拥有者决定

  • 容易获得锁:等待
  • 不易获得锁:直接挂起线程

(2)锁消除

虚拟机即时编译器在运行时,对一些代码要求同步,但是会消除被检测到的不可能存在共享数据竞争的锁。
如果判断到一段代码中,在堆上的数据不会逃逸出去被其他线程访问,则可以将其作为栈上数据对待,也就无需加锁

(3)锁粗化

  • 一般来说,同步块的范围应该尽量小,只在共享数据的实际作用域中进行同步,方便锁的释放
  • 如果一系列操作都对同一个对象反复加锁和释放锁,则应该扩大同步范围

(4)轻量级锁

HotSpot虚拟机的对象的内存布局

对象头

  • Mark World:存储对象自身的运行时数据,如哈希值,GC分代年龄等
  • 存储指向方法区对象类型数据的指针(和数组的长度)
轻量级锁的特点
  • 对于大部分锁,在整个同步周期内不存在竞争
  • 如果存在锁竞争,则轻量级锁比重量级锁慢(需要额外的CAS操作)
轻量级锁的实现
加锁:
  • 如果同步对象没有被锁定(标志位=01),虚拟机首先会在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Displaced Mark World拷贝
  • 虚拟机使用CAS操作尝试将对象的Mark World拷贝更新为指向Lock Record的指针
    • 更新成功,线程拥有对象的锁(标志位=00)
    • 更新失败,虚拟机检查Mark World是否指向当前线程的栈帧
      • 是:证明线程已经拥有了锁
      • 否:证明锁已经被其他线程抢占,如果有多个线程争夺锁,则锁为重量级锁(标志位=10),Mark World存储指向重量级锁的指针,等待的线程阻塞
解锁:
  • 如果对象的Mark World仍然指向线程的锁记录,则用CAS操作把当前对象的Mark World和线程中复制的Displaced Mark World替换
    • 替换成功:同步完成
    • 替换失败:在释放锁的同时,唤醒其他被挂起的线程

(5)偏向锁

消除数据在无竞争下的同步原语,进一步提高程序的运行性能

加锁
  • 如果线程第一次获取锁,则对象头启用偏向模式(标志位=01)
  • 使用CAS操作,把线程ID记录在对象头的Mark World中
    • 操作成功:持有锁的线程以后每次进入同步块时,无需同步操作
  • 线程结束操作,重偏向
撤销偏向

另外一个线程请求获取锁,偏向模式结束

  • 锁被其他线程锁定:恢复到轻量级锁状态(标志位=00)
  • 锁未被锁定:恢复到未锁定状态(标志位=01)
特点
  • 提高带有同步、无竞争的程序性能
  • 如果同一个锁有多个线程竞争,则偏向锁是多余的

猜你喜欢

转载自blog.csdn.net/weixin_36904568/article/details/90301235