[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释放锁

目录
相关文章
|
8天前
|
Java
Java基础—笔记—static篇
`static`关键字用于声明静态变量和方法,在类加载时初始化,只有一份共享内存。静态变量可通过类名或对象访问,但推荐使用类名。静态方法无`this`,不能访问实例成员,常用于工具类。静态代码块在类加载时执行一次,用于初始化静态成员。
10 0
|
8天前
|
Java API 索引
Java基础—笔记—String篇
本文介绍了Java中的`String`类、包的管理和API文档的使用。包用于分类管理Java程序,同包下类无需导包,不同包需导入。使用API时,可按类名搜索、查看包、介绍、构造器和方法。方法命名能暗示其功能,注意参数和返回值。`String`创建有两种方式:双引号创建(常量池,共享)和构造器`new`(每次新建对象)。此外,列举了`String`的常用方法,如`length()`、`charAt()`、`equals()`、`substring()`等。
14 0
|
9天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
13天前
|
Java 调度
Java并发编程:深入理解线程池的原理与实践
【4月更文挑战第6天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将从线程池的基本原理入手,逐步解析其工作过程,以及如何在实际开发中合理使用线程池以提高程序性能。同时,我们还将关注线程池的一些高级特性,如自定义线程工厂、拒绝策略等,以帮助读者更好地掌握线程池的使用技巧。
|
1天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
8天前
|
运维 NoSQL 算法
Java开发-深入理解Redis Cluster的工作原理
综上所述,Redis Cluster通过数据分片、节点发现、主从复制、数据迁移、故障检测和客户端路由等机制,实现了一个分布式的、高可用的Redis解决方案。它允许数据分布在多个节点上,提供了自动故障转移和读写分离的功能,适用于需要大规模、高性能、高可用性的应用场景。
15 0
|
8天前
|
Java API
Java基础—笔记—内部类、枚举、泛型篇
本文介绍了Java编程中的内部类、枚举和泛型概念。匿名内部类用于简化类的创建,常作为方法参数,其原理是生成一个隐含的子类。枚举用于表示有限的固定数量的值,常用于系统配置或switch语句中。泛型则用来在编译时增强类型安全性,接收特定数据类型,包括泛型类、泛型接口和泛型方法。
9 0
|
9天前
|
存储 安全 Java
java多线程之原子操作类
java多线程之原子操作类
|
10天前
|
算法 JavaScript Java
Java多线程+分治求和,太牛了
`shigen`,一位擅长Java、Python、Vue和Shell的博主,分享编程知识和成长体验。在一次面试中因对高并发问题准备不足而受挫,随后深入学习,研究了线程池和经典案例——计算1亿数字的和。采用分治策略,`shigen`实现了Java版的归并排序,并对比了Python的简洁实现。通过多线程和分段求和优化,展示了如何高效解决大数求和问题,引入了分治思想的递归任务来进一步提升性能。未来将探讨`forkjoin`框架。关注`shigen`,每天学习新知识!
17 0
Java多线程+分治求和,太牛了
|
11天前
|
Java
Java中的多线程实现:使用Thread类与Runnable接口
【4月更文挑战第8天】本文将详细介绍Java中实现多线程的两种方法:使用Thread类和实现Runnable接口。我们将通过实例代码展示如何创建和管理线程,以及如何处理线程同步问题。最后,我们将比较这两种方法的优缺点,以帮助读者在实际开发中选择合适的多线程实现方式。
19 4

热门文章

最新文章