[jjzhu学java之多线程笔记]java并发机制的底层实现原理

简介: volative的应用volatile的定义与实现原理synchronized的实现原理和应用java对象头锁升级偏向锁偏向锁的撤销关闭偏向锁轻量锁轻量锁加锁轻量锁解锁锁的优缺点对比原子操作的实现原理术语定义处理器实现原子操作使用总线锁保证原子性使用缓存锁保证原子性java如何实现原子操作

volative的应用

volatile的定义与实现原理

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

术语 英文单词 术语描述
内存屏障 memory barriers 一组处理器指令,用于实现对内存操作的顺序限制
缓冲行 cache line 缓存中可以分配的最小存储单位。
原子操作 atomic operations 不可中断的一个或一系列操作

有volatile变量修饰的共享变量进行写操作的时候会多出一些汇编代码,加入Lock前缀。Lock前缀的指令在多核处理器会引发两件事情
1. 将当前处理器缓存行的数据写回到系统内存
2. 这个写回内存的操作会使在其他cpu里缓存了该内存地址的数据无效。

在多处理器下,为了保证各个处理器的缓存是一致的,会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

volatile的两条实现原则
1. Lock前缀指令会引起处理器缓存回写到内存
2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效

synchronized的实现原理和应用

Java中的每一个对象都可以作为锁。具体表现为以下3种形式:
1. 对于普通同步方法,锁是当前实例对象。
2. 对于静态同步方法,锁是当前类的Class对象。
3. 对于同步方法块,锁是Synchonized括号里配置的对象。

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

java对象头

synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽
(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit

image

Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。32位JVM
的Mark Word的默认存储结构如下图示
image

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

在64位虚拟机下,Mark Word是64bit大小的,如下图示
image

锁升级

Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:
1. 无锁状态
2. 偏向锁状态
3. 轻量级锁状态
4. 重量级锁状态

这几个状态会随着竞争情况逐渐升级。

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同
一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
当出现偏向锁竞争的时候,按如下步骤执行
1. 暂停拥有偏向锁的线程
2. 检查持有偏向锁的线程是否还alive,若不是,则将对象头设置为无锁状态,否则执行3
3. 线程仍然活着,执行偏向锁的栈,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么回复到无锁或者标记对象不适合作为偏向锁
4. 唤醒暂停的线程

image

关闭偏向锁

偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如
有必要可以使用JVM参数来关闭延迟:

-XX:BiasedLockingStartupDelay=0

如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:

-XX:-UseBiasedLocking=false

那么程序默认会进入轻量级锁状态。

轻量锁

轻量锁加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失 败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

displaced mark word是整个轻量级锁实现的关键,在CAS中的compare就需要用它作为条件。在拷贝完object mark word之后,JVM做了一步交换指针的操作将object mark word里的轻量级锁指针指向lock record所在的stack指针,作用是让其他线程知道,该object monitor已被占用(就像偏向锁中用CAS的方式将mark word的id指向当前尝试获取锁的线程id,这里是将mark word中的轻量级锁指针以CAS的方式尝试指向当前线程的lock record,这样别的线程便知道当前轻量锁已经指向别的线程了)。lock record里的owner指针指向object mark word的作用是为了在接下里的运行过程中,识别哪个对象被锁住了。
image
image

轻量锁解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成
功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
轻量锁的膨胀流程如下图示
image
image
所以由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的mark word,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对mark word做了修改,两者比对发现不一致,则切换到重量锁。
因为重量级锁被修改了,所有display mark word和原来的mark word不一样了。

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。

锁的优缺点对比

image

原子操作的实现原理

术语定义

术语名称 英文 解释
缓存行 Cache line 缓存的最小操作单位
比较并交换 Compare and Swap CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有,这换成新值,否则不交换
CPU流水线 CPU pipeline
内存顺序冲突 Memory order violation 内存顺序冲突一般是由假共享引起的,假共享是指对个CPU同时修改同一个缓存行的不同部分而引起的其中一个cpu的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线

处理器实现原子操作

使用总线锁保证原子性

如果多个处理器同时对共享变量进行读改写操作
(i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操
作就不是原子的,操作完之后共享变量的值会和期望的不一致。
image
所谓总线锁就是使用处理器提供的一个
LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该
处理器可以独占共享内存。

使用缓存锁保证原子性

第二个机制是通过缓存锁定来保证原子性。在同一时刻,我们只需保证对某个内存地址
的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处
理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存
行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效

有两种情况下处理器不会使用缓存锁定:
1. 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行
(cache line)时,则处理器会调用总线锁定
2. 有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的
内存区域在处理器的缓存行中也会调用总线锁定

java如何实现原子操作

在Java中可以通过锁和循环CAS的方式来实现原子操作
1. 使用循环CAS实现原子操作
2. CAS实现原子操作的三大问题
- ABA问题

因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化
则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它
的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面
追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从
Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题
- 循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
- 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循
环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。
3. 使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁
机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁

目录
相关文章
|
11天前
|
XML 安全 Java
Java反射机制:解锁代码的无限可能
Java 反射(Reflection)是Java 的特征之一,它允许程序在运行时动态地访问和操作类的信息,包括类的属性、方法和构造函数。 反射机制能够使程序具备更大的灵活性和扩展性
19 5
Java反射机制:解锁代码的无限可能
|
4天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
5天前
|
安全 IDE Java
Java反射Reflect机制详解
Java反射(Reflection)机制是Java语言的重要特性之一,允许程序在运行时动态地获取类的信息,并对类进行操作,如创建实例、调用方法、访问字段等。反射机制极大地提高了Java程序的灵活性和动态性,但也带来了性能和安全方面的挑战。本文将详细介绍Java反射机制的基本概念、常用操作、应用场景以及其优缺点。 ## 基本概念 ### 什么是反射 反射是一种在程序运行时动态获取类的信息,并对类进行操作的机制。通过反射,程序可以在运行时获得类的字段、方法、构造函数等信息,并可以动态调用方法、创建实例和访问字段。 ### 反射的核心类 Java反射机制主要由以下几个类和接口组成,这些类
16 2
|
9天前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
23 2
|
10天前
|
存储 缓存 安全
🌟Java零基础:深入解析Java序列化机制
【10月更文挑战第20天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
19 3
|
10天前
|
安全 Java UED
深入理解Java中的异常处理机制
【10月更文挑战第25天】在编程世界中,错误和意外是不可避免的。Java作为一种广泛使用的编程语言,其异常处理机制是确保程序健壮性和可靠性的关键。本文通过浅显易懂的语言和实际示例,引导读者了解Java异常处理的基本概念、分类以及如何有效地使用try-catch-finally语句来处理异常情况。我们将从一个简单的例子开始,逐步深入到异常处理的最佳实践,旨在帮助初学者和有经验的开发者更好地掌握这一重要技能。
16 2
|
12天前
|
Java 数据库连接 开发者
Java中的异常处理机制####
本文深入探讨了Java语言中异常处理的核心概念,通过实例解析了try-catch语句的工作原理,并讨论了finally块和throws关键字的使用场景。我们将了解如何在Java程序中有效地管理错误,提高代码的健壮性和可维护性。 ####
|
15天前
|
安全 Java 程序员
深入浅出Java中的异常处理机制
【10月更文挑战第20天】本文将带你一探Java的异常处理世界,通过浅显易懂的语言和生动的比喻,让你在轻松阅读中掌握Java异常处理的核心概念。我们将一起学习如何优雅地处理代码中不可预见的错误,确保程序的健壮性和稳定性。准备好了吗?让我们一起踏上这段旅程吧!
22 6
|
12天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
23 1
|
15天前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
37 5