【并发编程的艺术】JAVA并发机制的底层原理

简介: Java代码的执行过程:代码编译->Java字节码->类加载器加载到JVM->JVM执行字节码,最终转化为汇编指令在CPU中执行。所以,Java中使用的并发机制,也依赖于JVM的实现和CPU指令。本章将重点描述这两个关键字的实现,并由此深入探索操作系统底层原理。

系列文章:

【并发编程的艺术】JVM 体系与内存模型

一 概述

   在上一篇文章,概述了JVM体系结构和内存模型的基础概念,我们了解到synchronized 和 volatile都属于内存模型中,处理可见性、顺序性、一致性等问题的关键策略,这又涉及到操作系统层面。

   Java代码的执行过程:代码编译->Java字节码->类加载器加载到JVM->JVM执行字节码,最终转化为汇编指令在CPU中执行。所以,Java中使用的并发机制,也依赖于JVM的实现和CPU指令。本章将重点描述这两个关键字的实现,并由此深入探索操作系统底层原理。

二 背景知识

2.1 相关CPU术语定义

下面表格来自《Java并发编程的艺术》:

2.2 CPU多级缓存

关于CPU多级缓存结构,示意图如下:

Intel Core i7的高速缓存层次结构如下图所示:

这里涉及到几个问题:

1、为什么需要cache?

为了缓解cpu和内存速度不匹配问题。如果没有缓存,那么处理器时钟周期内,CPU需要常常等待主存,这会导致浪费cpu资源。

2、为什么需要多级缓存?

成本与效率的折衷考虑。越接近CPU的缓存速度越快,但相应的成本也会越高,所以不会设置很大(否则直接替换内存就好了)。

另外,L1 Cache还分为L1 i-Cache 和 L1 d-Cache,其中L1 i-cache存储指令,是只读的; L1 d-cache存储数据,是读写的。

3、带来哪些问题?

3-1 一致性,多级缓存和内存之间,共享数据如何保障一致性

3-2 乱序执行。处理器为了提高运算速度,可能会做出违背代码原有顺序的优化。

更详细的CPU缓存分析,可参考文章: CPU Cache。本文不再做展开描述。

三 volatile

   大家应该都或多或少了解过volatile的含义或作用,例如volatile可以理解为轻量级的synchronized,用于在多处理器开发时,保证共享变量的可见性。如果volatile使用得当,在某些场景可以避免使用synchronized,等等。

3.1 定义

   下面我们给出官方对volatile的定义,Java语言规范和虚拟机规范官方文档valatile field

The Java programming language allows threads to access shared variables (§17.1). As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.

The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.

A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable (§17.4).

翻译过来:

   Java编程语言允许多线程访问共享变量。通常,为了确保共享变量得到一致和可靠地更新,线程应该通过获得一个排它锁来确保它独占使用这些变量,按照惯例,该锁强制这些共享变量互斥。

   一个字段可以被声明为volatile,当Java内存模型确保所有线程对这个变量看到的值是一致的。

3.2 volatile的可见性保障

x64处理器下,通过jit编译器获取下面代码的汇编指令,来分析CPU做了什么。代码示例:

private volatile instance = new Singleton();  //java单例模式实现中的一种

对应的汇编命令:

注意其中的lock,lock前缀的指令在多核处理器下会引发两件事:

(1)将当前处理器缓存行的数据写回到系统内存

(2)这个写回内存的操作,会使在其他CPU里缓存了该内存地址的数据无效。

在变量声明volatile之后,如果对这个变量进行了写操作,JVM就会向cpu发送一条lock前缀的指令,将这个变量所在缓存行的数据写回系统内存(注意,这时其他处理器缓存的值还是旧的,如果还使用这个旧值就会出问题);为了保证各cpu的缓存一致,就需要实现缓存一致性协议(MESI):每个CPU通过嗅探在总线上传播的数据来检查自己缓存的值是否过期,当发现自己缓存行对应的内存地址被修改,就会把当前处理器的缓存行设置为无效状态;当CPU对这个数据进行修改操作时,会重新从系统内存中读取数据到CPU缓存。

完整资料可查看What Every Programmer Should Know About Memory。后续会考虑翻译部分关键内容。简单了解,也可以参考文章MESI 缓存一致性协议(翻译,中英对照)

四 synchronized

   synchronized,很多人称为重量级锁,但在Java SE 1.6对其进行了一系列优化之后,很多情况下就没那么重了。主要包括为了减少获得锁和释放锁带来的性能消耗,而引入的偏向锁和轻量级锁,以及锁升级机制。

4.1 使用基础

Java中每个对象都可以作为锁;

线程试图访问同步代码块时,必须先获得锁,退出或抛出异常时必须释放锁。

3种使用方式和锁定的内容:

1、普通同步方法,锁的是当前实例;

2、静态同步方法,锁的是当前类的Class对象;

3、同步方法块,锁的是synchronized括号里配置的对象。

4.2 锁的本质

   synchronized,在JVM中基于进入和退出Monitor对象来实现方法同步和代码块同步,二者实现细节稍有不同。代码块同步使用monitorenter和monitorexit指令来实现的,方法同步是另一种方式,细节在JVM规范中没有详细说明(待考证)。

   synchronized用的锁,是在Java对象头里面的。数组类型比较特殊,用3个字宽存储对象头;非数组对象2字宽。32位虚拟机中,1字宽=4byte=32bit。对象头结构:

其中,Mark Word默认存储对象的HashCode、分代年龄和锁标志位(以下都是32位虚拟机下的情况)

运行期间,Mark Word存储的数据会随着锁标志位的变化而变化。可能变化为存储以下4种数据:

其中,偏向锁、轻量级锁、重量级锁就是锁升级机制的重要组成部分。

64位虚拟机下,Mark Word是64bit大小,结构如下图:

4.3 锁升级机制

锁的四种状态,级别由低到高:无锁状态、偏向锁、轻量级锁、重量级锁。状态会随竞争情况逐渐升级。注意,升级的方向只能从低到高,没有降级策略。

4.3.1 偏向锁

实现:对象头和栈帧中的锁记录里,存储锁偏向的线程id。撤销机制:等到竞争出现,即其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。并且需要等到全局安全点(此时没有正在执行的字节码)。

获取和撤销流程见下图:

4.3.2 轻量级锁与重量级锁

   线程执行同步代码块前,JVM在当前线程的栈帧中创建用语存储锁记录的空间,并将对象头的Mark Word复制到锁记录中。然后线程尝试CAS将对象头中的Mark Word替换为指向所记录的指针。成功表示当前线程获得锁,否则表示其他线程竞争锁,当前线程通过自旋来获取锁。

   解锁时,CAS操作将Displaced Mark Word替换回到对象头,如果成功则表示没有竞争;失败则表示存在锁竞争,会继续膨胀为重量级锁。下图是锁竞争导致膨胀的流程:

注:上面我们提到竞争时线程会尝试自旋来获取锁,而自旋会消耗CPU资源。所以一旦锁升级成重量级锁,就不会再恢复到轻量级锁。当锁处于重量级状态下,而其他线程尝试获取锁时,都会被阻塞,只有当持有锁的线程释放锁时才唤醒这些线程,并进行下一轮的锁竞争。

4.3.3 锁优缺点分析

   通过资源消耗、线程是否阻塞、响应耗时等角度分析,偏向锁、轻量级锁、重量级锁各自的优缺点如下表所示:

五 总结

   本文是并发编程系列的第二篇。通过上篇文章,对JVM体系结构和内存模型有了一些了解的基础上,开始介绍内存模型中的锁、可见性的深入分析。在下一篇的文章中,我们将继续分析原子性、和顺序一致性等其他内容。

参考资料:

CPU多级缓存

CPU Cache

MESI 缓存一致性协议(翻译,中英对照)

What Every Programmer Should Know About Memory

相关文章
|
10天前
|
存储 Java 关系型数据库
高效连接之道:Java连接池原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。频繁创建和关闭连接会消耗大量资源,导致性能瓶颈。为此,Java连接池技术通过复用连接,实现高效、稳定的数据库连接管理。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接池的基本操作、配置和使用方法,以及在电商应用中的具体应用示例。
27 5
|
1天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
10天前
|
XML 安全 Java
Java反射机制:解锁代码的无限可能
Java 反射(Reflection)是Java 的特征之一,它允许程序在运行时动态地访问和操作类的信息,包括类的属性、方法和构造函数。 反射机制能够使程序具备更大的灵活性和扩展性
19 5
Java反射机制:解锁代码的无限可能
|
3天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
4天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
16 2
|
4天前
|
安全 IDE Java
Java反射Reflect机制详解
Java反射(Reflection)机制是Java语言的重要特性之一,允许程序在运行时动态地获取类的信息,并对类进行操作,如创建实例、调用方法、访问字段等。反射机制极大地提高了Java程序的灵活性和动态性,但也带来了性能和安全方面的挑战。本文将详细介绍Java反射机制的基本概念、常用操作、应用场景以及其优缺点。 ## 基本概念 ### 什么是反射 反射是一种在程序运行时动态获取类的信息,并对类进行操作的机制。通过反射,程序可以在运行时获得类的字段、方法、构造函数等信息,并可以动态调用方法、创建实例和访问字段。 ### 反射的核心类 Java反射机制主要由以下几个类和接口组成,这些类
11 2
|
7天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
20 2
|
7天前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
21 2
|
9天前
|
存储 缓存 安全
🌟Java零基础:深入解析Java序列化机制
【10月更文挑战第20天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
18 3
|
7天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
22 1