JVM的高效并发

1. 为什么要并发、并发导致了什么问题

CPU、内存、IO设备三者之间的速度差异一直是一个核心矛盾,为了解决缓解这一差异,计算机系统架构上作出了以下优化:

  1. CPU增加多级高速缓存,作为内存和CPU之间的缓冲,以缓解CPU和内存之间的速度差异;
  2. 操作系统增加进程、线程,以分时复用CPU;
  3. CPU乱序执行、编译器指令重排,以最大程度利用CPU的各个运算单元。

缓存导致可见性问题

单核时代的CPU缓存与内存的数据一致性比较容易解决,因为操作的都是同一份缓存。

在多核时代,每个CPU都有自己的缓存,多个线程在不同内核上执行时,操作的是不同的缓存,这就产生了可见性问题。

CPU利用缓存一致性协议来保证可见性,Java使用内存模型保证可见性和有序性,提供Happens-Before语义,使用volatile可以保证多线程的可见性与有序性,synchronized也具有可见性,但只针对synchronized同步的共享对象。

编译器优化导致有序性问题

有序性是指程序按照代码的先后顺序执行、保持串行语义。编译器优化会进行指令重排,改变程序中语句的先后顺序。

比如,在对象创建过程中,执行构造器、连接引用和实例可能会被重排序,导致其它线程获取到了一个没有被完全初始化的对象。

volatile和synchronized都可以保证有序性。volatile包含禁止指令重排的语义;而synchronized则通过一个对象同时只允许一个线程对其lock操作这个规则,表现在同步快只能有序的串行进入。

线程切换导致原子性问题

一个或多个操作的执行过程不被中断、干扰称之为原子性。操作系统可以在任意一条CPU指令结束之后进行线程切换,而高级语言里的一条语句往往需要多个CPU指令才能完成,所以CPU只能保证指令的原子性而不能保证语言级别一条语句的原子性。

原子性一般通过同步机制(互斥锁)实现,保证相关操作不会被其它线程干扰,达到对共享资源的互斥操作。在Java中,基本数据类型的读写都具备原子性,如果需要更大范围的原子性保证,可以使用字节码monitorenter和monitorexit,对应着synchronized关键字。

在这里插入图片描述

2. 内存模型

内存模型主要关注各种变量的访问规则,即虚拟机如何从内存中读取和存储变量。

Java内存模模型划分主内存和工作内存

  1. 主内存可以简单类比为物理上的内存,所有变量均需要存储与主内存。
  2. 工作内存可以类比为高速缓存,每个线程都可以拥有自己的工作内存,作为主内存的副本。

同时定义了read、write、lock、unlock等在执行引擎、工作内存和主内存之间的变量交互操作。

2.1 可见性和有序性

导致可见性的原因在于缓存,导致有序性的原因在于指令重排,那么解决可见性和有序性最直接的办法就是按需禁用缓存以及指令重排序

Java内存模型提供了按需禁用的方法,包括volatile、synchronized和final关键字,以及Happens-Before原则,提供可见性和有序性保证

2.2 volatile

当变量被定义为volatile后,将具备两项特性:

可见性

当一个线程修改该变量,新值对其它线程来说立刻可见,不存在一致性问题。但Java运算符并不是原子操作,对volatile变量的运算(比如依赖于当前值的自增操作)在并发下依然不安全,此时需要加锁。

在实现上,通过将赋值操作实时刷新到主内存完成,该操作也会使得其它内核的缓存无效。

有序性

禁止指令重排序,是的程序按照代码顺序执行。

在实现上,是通过在volatile变量赋值操作之后添加一个内存屏障,指令重排序时不能将后面的指令重排序到内存屏障之前。

2.3 Happens-Before

Happens-Before的意思是前一个操作的结果对后一个操作可见,本质上是一种可见性约束。如果A Happens-Before 于 B,那么A的结果对B可见,无论AB两个事件是否在同一个线程内。

Happens-Before的规则主要有:

  • 程序的顺序规则

    在同一个线程内,按照代码顺序,前面的操作Happens-Before于后面的任何操作。

  • 传递性

    如果A Happens-Before 于 B,B Happens-Before 于 C,那么A Happens-Before 于 C。

  • volatile

    对volatile的写操作,Happens-Before于后续对这个变量的读操作,新值立刻可见。

3. 线程

3.1 Java 线程的实现

线程的实现方式主要有:内核线程、用户线程(非内核线程,协程)、混合线程,Java线程采用内核线程实现,所有线程直接映射到操作系统原生线程实现,由内核负责管理,并通过抢占式调度。

线程的状态有:New、Runnable、Blocked、Waiting、Timed Waiting、Terminated。

协程的复苏

传统的内核线程调度成本高、数量有限,相对来说比较重量级。

协程完全建立在用户空间,由程序自己实现调度,无需内核参与,显得十分轻量。

3.2 线程安全

允许被多个线程同时执行的代码称为线程安全的代码。当多个线程访问某一个类(对象或方法)时,这个类始终能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。

Java中不可变对象一定是线程安全的。

互斥同步是最常见的并发正确性的保障手段,在Java里最基本的互斥同步手段就是synchronized关键字,这是一种块结构的同步语法,编译完后会形成monitorenter和monitorexit两个字节码指令。

持有锁是一个重量级的操作,Java中的线程使用操作系统原生的内核线程实现,除了锁维护本身的开销,阻塞或者唤醒一个线程都不可避免的需要在用户态和内核态进行切换。虚拟机也会进行一些优化,比如在阻塞线程之前加入一段自旋等待过程,避免频繁的切入到内核态。

非阻塞同步相较于互斥同步的另一种并发安全的处理策略。互斥同步属于一种悲观的并发策略,其主要面临的问题是线程阻塞和唤醒所带来性能开销。因此出现了CAS等需要硬件指令集支持的、基于冲突检测的乐观并发策略——先进行操作,如果没有冲突就擦欧总成功;如果共享数据产生了冲突,那么才采用其它补救策略。

乐观的并发策略需要指令集支持,因为需要保证操作和冲突检测两个步骤的原子性

3.3 锁优化(synchronized)

HotSpot主要利用对象头的MarkWord存储空间、CAS操作等手段,避免在轻度竞争、甚至不存在竞争的情况下使用传统的、操作系统互斥量实现的重量级锁,减少同步开销。

偏向锁

偏向锁的偏是指该锁会偏向于第一个获得它的线程,如果在接下来的过程中,该锁一直没有被其它线程获取,则持有偏向锁的线程在重入时无需任何同步操作。

当锁对象第一次被线程获取时,会使用CAS将线程的ID记录在MarkWord中,如果CAS操作成功,则该线程获取到了偏向锁。

轻量级锁

轻量级是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制也称为”重量级锁“。当有两个不同的线程获取锁对象时,偏向锁便会被撤销,升级为轻量级锁。

轻量级锁也是利用了对象头的MarkWord空间,虚拟机会在线程的栈帧中创建锁记录,利用CAS将MarkWord更改为指向锁记录的指针,如果成功,则该线程获得轻量级锁。

自适应自旋

如果有两个以上的线程竞争锁对象时,轻量级锁会膨胀为重量级锁。但是重量级锁的挂起和恢复线程存在内核态切换,会给并发性能带来很大的压力。

考虑到,在大部分场景下,共享数据的锁定状态只会持续很短一段时间,因此,可以让后面的线程等一等,看看持有锁的线程会不会很快释放锁,这个就是一个自旋操作。

自旋会消耗CPU时间,固定的自旋次数可能并不能很好的适用所有场景。JDK 6 提出了自适应自旋,根据该锁对象的历史自旋状况,决定当前是否自旋以及自旋的次数。

自旋仍然没有获取到锁的话,线程会陷入内核态,申请一个互斥量,并将MarkWord更新为指向互斥量的指针,并进入阻塞状态。当持有线程的锁释放时,发现了MarkWord的变更,会唤醒该互斥量上的线程。

锁消除

锁消除是指JVM对于一些要求同步,但是却不存在共享变量竞争的锁进行消除。

锁消除的主要判断依据来源于逃逸分析,如果代码中的对象不会逃逸出去被其它线程访问,那么就无需进行同步。

锁粗化

原则上,在编码的过程中总是推荐将锁的作用范围控制的尽可能小,降低锁的持有时间。

但是如果存在一系列的连续操作对同一个对象反复加锁和解锁,甚至加锁操作位于循环体之中,频繁的锁操作也会造成不必要的性能损失。

锁粗化是虚拟机在监测到有一连串的操作都对同一个对象加锁,就会将锁的范围扩大(粗化)到整个操作的外部。

锁升级、锁降级

随着竞争的加剧,锁会从偏向锁、轻量级锁、重量级锁的顺序进行升级,同时,在安全点位置,JVM会检查是否有闲置的Monitor,并试图进行降级。

参考

《Java并发编程实战》极客时间

《深入理解Java虚拟机:JVM高级特性与最佳实践》(第三版)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值