HotSpot虚拟机之内存模型与线程安全

news2024/7/6 18:12:40

目录

一、线程内存模型

1. 内存模型

2. 内存模型操作

二、Happens-Before原则

三、Java线程

1. 线程实现方式

2. Java线程状态

四、Java线程安全

1. 线程安全程度

2. 锁优化

五、参考资料


一、线程内存模型

1. 内存模型

        内存模型主要目的是定义共享变量的访问规则,共享变量如:实例字段、静态字段、数组元素等线程共享变量(不包含线程私有变量)。内存模型中有:主内存、工作内存,如下图所示是两者交互关系,看出主内存直接对应于物理硬件的内存,而程序运行时主要访问的是工作内存

  • 主内存(Main Memory):内存模型规定所有变量都存储在主内存中
  • 工作内存(Working Memory):每个线程都有自己的内存,且变量是主内存的副本

        线程对变量的操作只能在工作内存,无法直接读写主内存;线程之间变量传递必须通过主内存实现,模型如下图所示。

        注意:内存模型与内存区域划分没有任何关系,若勉强有关系,则:主内存对应堆对象实例,工作内存对应JVM栈的部分区域。

2. 内存模型操作

        内存模型操作,即:主内存与工作内存交互,定义了8种原子性操作:lock、unlock、read、load、use、assign、store、write,如下表所示。

内存模型操作

特点

lock

(锁定)

范围:主内存变量;

作用:把变量标识为一个线程独占的状态。

unlock

(解锁)

范围:主内存变量;

作用:释放处于锁定状态的变量,后才能被其他线程锁定。

read

(读取)

范围:主内存变量;

作用:把变量的值从主内存传输到线程工作内存中,以便后续load操作。

load

(载入)

范围:工作内存变量;

作用:把read操作获取的变量值存储到工作内存的变量副本中

use

(使用)

范围:工作内存变量;

作用:把变量的值传递给执行引擎(每遇到使用该变量的字节码指令)。

assign

(赋值)

范围:工作内存变量;

作用:把执行引擎接收到的值赋给工作内存的变量

      (每遇到给变量赋值的字节码指令)。

store

(存储)

范围:工作内存变量;

作用:把变量的值从工作内存传输到主内存中,以便后续write操作。

write

(写入)

范围:主内存变量;

作用:把store操作获取的变量值存储到主内存中。

注意:

    a.一个变量从主内存复制到工作内存:必须顺序执行read、load操作,但可以不连续执行

       一个变量从工作内存同步到主内存:必须顺序执行store、write操作,但可以不连续执行

    b.8种操作满足以下规则:

       1):read和load、store和write不允许单独出现,即:不会出现回写主内存但其不接受;

       2):不允许线程丢弃最近的assign,即:工作内存值改变,则必须同步到主内存;

       3):不允许线程不原因的同步到主内存,即:不允许没有assign操作就同步到主内存;

       4):一个新变量主内存诞生,即:对变量进行use、store时,则必须先执行assign、load;

       5):同一时刻只有一个线程对变量lock(同一线程多次lock,则必须多次unlock后才释放);

       6):执行lock时,则必须清空工作内存中此变量的副本值,后使用时重新执行load、assign;

       7):执行unlock之前,则必须把此变量同步到主内存中;

    c.Java内存模型操作简化为:read、write、lock、unlock的四种操作

        从上表看出,内存模型主要围绕并发过程中如何处理原子性、可见性、有序性建立的,这三大特性如下图所示。

        注意:只有一条字节码指令也不意味着是原子性,解释器要运行多行代码才能实现其语义;volatile修饰的变量具有特性:可见性、禁止指令重排序;"long和double的非原子协定":没有volatile修饰的64位数据的读写操作划分为两次32位操作,但一般认为是原子操作(概率极低)。 

二、Happens-Before原则

        Happens-Before原则(先行发生原则)定义两操作之间的偏序关系,因此并发安全问题不要受时间顺序影响,一切按先行发生原则为准。无需任何同步手段保证先行发生规则,如下表所示。

先行发生原则

特点

程序次序规则

同一个线程内,按照控制流顺序,在前的操作先行发生于其后的操作

管程锁定规则

释放锁操作先行发生于同一个锁的加锁操作

volatile变量规则

volatile变量写操作先行发生于读操作

线程启动规则

Thread线程start()方法先行发生于此线程的每一个动作

线程终止规则

线程中所有操作先行发生于对此线程的终止检查

线程中断规则

线程interrupt()方法调用先行发生于被中断代码检查到中断时间的发生

对象终结规则

对象构造函数完成先行发生于它的finalize()方法的开始

传递性

操作A先行发生于操作B,操作B先行发生于操作C,则A先行发生于C

三、Java线程

1. 线程实现方式

        线程是轻量级进程,各个线程共享进程资源(内存地址、I/O等)、又可以独立调度,把一个进程的资源分配和执行调用分开。实现线程有3种方式:内核线程实现(1:1)、用户线程实现(N:1)、混合实现(M:N),如下表所示。

线程实现

特点

内核线程实现

(1:1)

1.“内核线程”:直接由OS内核完成,内核完成线程切换,操纵调度器对线程调度,并负责将线程的任务映射到各个CPU上(每个内核线程可以视为内核的一个分身)

2.每个轻量级进程(线程)都有一个内核线程支持,即:1:1实现

3.缺点:

   a.系统调用代价大,需要在用户态与内核态来回切换;

   b.OS支持轻量级进程的数量有限;

4.Java线程采用内核线程实现

用户线程实现

(N:1)

1.“用户线程”:线程非内核线程,线程的创建、同步、销毁及调度在用户态中完成无需内核的帮助,映射到一个CPU上,即:N:1实现

2.优点:无需切换到内核,因此速度快、低耗;更大规模的线程数;

  缺点:线程调度实现复杂;增大线程被阻塞的风险。

混合实现

(M:N)

内核和用户线程混合使用,用户负责线程的创建、同步、销毁;内核线程负责线程调度

        Java线程实现方式采用内核线程实现,每一个java线程都直接映射到一个内核线程上,HotSpot不会干涉线程的调度

        线程调度(Scheduler)是指线程分配处理器使用权的过程,两种调度方式:协同式(协程)、抢占式(java采用),如下表所示。

实现方式

特点

协同式线程调度 - 协程

(Cooperative Threads-Scheduling)

1.线程执行时间由线程本身控制,线程工作执行完后,主动通知系统切换到另外的线程上

2.优点:切换操作对线程可知;实现简单;

   缺点:线程执行时间不可控,若代码有问题,则一直阻塞

3.应用:Lua语言的“协同例程”。

抢占式线程调度 - Java采用

(Preemptive Threads-Scheduling)

1.线程执行时间由系统来分配执行时间,如:Thread::yeild()方法可以主动让出时间,但无法主动获取执行时间

2.通过线程优先级可以“建议”OS多分配执行时间,但是不能稳定,最终还是OS决定。

        Java线程调度方式采用抢占式线程调度,因此Java中不能通过线程优先级完全准确判定一组Ready状态的线程会先执行哪一个。而Thread类大部分API都是Native修饰,而Native往往是该方法没有使用或无法使用平台无关的手段来实现

2. Java线程状态

        Java线程状态有6种状态:新建、运行(Runnable = Running + Ready)、无限期等待、限期等待、阻塞、结束,如下表所示。

线程状态

特点

新建

(New)

创建后但尚未启动,即:new之后,start()之前

运行

(Running + Ready)

包含两种状态:正在运行Running、正在等待系统分配执行时间Ready

无限期等待

(Waiting)

1.处于:线程不会被分配处理器执行时间,需被其他线程显示唤醒

2.方法有:

   没有设置timeout参数的Object::wait(),若加锁会释放锁;

   没有设置timeout参数的Object::join();

   LockSupport::park()。

限期等待

(Timed Waiting)

1.处于:线程不会被分配处理器执行时间,无需被其他线程显示唤醒,在一定时间之后系统会自动唤醒

2.方法有:

   Thread::sleep(),若加锁不会释放锁

   设置timeout参数的Object::wait(),若加锁会释放锁

   设置timeout参数的Object::join();

   LockSupport::parkNanos()、LockSupport::parkUntil()。

阻塞

(Blocked)

1.处于:线程被阻塞,需等待获取排他锁

2.“阻塞状态”与“等待状态”的区别:

   阻塞状态:需等待获取排他锁,建立在另一线程释放锁之上;

   等待状态:等待一段时间或唤醒动作的发生

结束

(Terminated)

已终止线程的状态

        任意时间点,线程有且只有其中一种状态,6种状态之间切换关系如下图所示。

四、Java线程安全

1. 线程安全程度

        多线程访问同一对象,不用考虑线程运行环境时的调度和交替执行,也不使用同步手段或调用方不进行协调操作时,使用该对象都能获取正确的结果,则称该对象是线程安全的。根据安全层度分为5种(依次降低):不可变、绝对线程安全、相对线程安全、线程兼容、线程对立,如下表所示。

线程安全程度

特点

不可变

1.不可变的共享对象,一定是线程安全的,无论是对象的方法还是调用者;

2.共享数据是基本数据类型,用final修饰来保证不可变

   共享数据是对象数据类型,需要对象自行保证自己的行为对其不受任何影响,如:String对象的substring()、replace()、concat()不会影响原值,只返回一个新构造的字符串对象

3.不可变对象有:final修饰的基本类型、String、AtomicInteger、AtomicLong。

绝对线程安全

1.“绝对安全”:不管运行如何,调用者都无需任何额外的同步手段

2.Java中绝大多数都不是绝对安全,而是相对安全。

相对线程安全

1.“相对安全”:对象单次操作是线程安全的,调用时无需额外的同步手段;

2.若是连续调用,则需要调用端额外的同步手段

3.相对线程安全类:Vector、HashTable等。

线程兼容

1.“线程兼容”:对象本身不是线程安全,需要调用端额外的同步手段

2.线程兼容的类:ArrayList、HashMap等

线程对立

1.“线程对立”:无论是否同步,都无法在多线程环境并发使用

2.Java天生支持多线程特性,应避免尽可能避免线程对立,会出现死锁。

        注意:线程安全是以多线程之间存在共享数据为前提;不可变对象,如:String对象的substring()、replace()、concat()不影响原值,只返回一个新构造的字符串对象

        现实线程安全有3种方式:互斥同步(阻塞同步)、非阻塞同步、无同步,如下表所示。

线程安全现实

特点

互斥同步

(阻塞同步)

1.“同步”:多线程并发访问时,保证共享数据同一时刻只能被一个线程使用

2.互斥是实现同步的手段,如:互斥量、信号量、临界区等手段;

3.sychronized(重量级)实现互斥(monitorenter、monitorexit两指令完成):

   a.当前线程持有锁后,锁计数器+1;而monitorexit则锁计数器减一,直到计数器为0时,才释放锁

   b.可重入,即:同一线程反复进入同步块不会出现自锁现象

   c.当前持有锁线程没有释放锁之前,其他线程无条件的被阻塞;

4.Lock接口(轻量级):

   a.必须在finally块中手动释放锁;

   b.获取锁时,可以超时中断;

   c.可以实现公平锁、非公平锁;

   d.锁绑定多个条件;

5.互斥同步属于悲观的并发策略

6.缺点:线程阻塞和唤醒带来的性能开销。

非阻塞同步

1.基于冲突检查的乐观并发策略,共享数据检查到冲突,进行补偿措施(如重试)达到一致,不需要其他线程挂起

2.常用处理器指令集:比较并交换(CAS_常用)、交换Swap等;

3.CAS:x86指令集使用cmpxchg完成CAS;“ABA问题”,解决:时间戳控制版本。

无同步

1.线程安全的代码无需同步,如:可重入代码、ThreadLocal;

2.“可重入代码”:代码执行的任何时候中断,去执行另一段代码,而控制权返回时原程序不会出现任何错误和对结果的影响;

3.所有可重入代码是线程安全的;但是线性安全的代码不一定是可重入代码。

        注意:sychronized可重入,即:同一线程反复进入同步块不会出现自锁现象,当前持有锁线程没有释放锁之前,其他线程无条件的被阻塞;JDK5后类库使用CAS操作(Unsafe类完成)但是用户无法使用CAS;JDK9后VarHandle类开放面向程序使用CAS

2. 锁优化

        JDK6各种锁优化技术:自旋锁、自适应自旋锁、锁消除、锁粗化、轻量级锁、偏向锁,如下表所示。

锁优化

特点

自旋锁

1.“自旋锁”:等待获取锁的阻塞线程,执行忙循环(自旋),而不是切换线程

2.自旋等待避免线程切换的开销,但是占用CPU处理时间,因此:自旋超出限定次数仍没有成功,则线程挂起

3.开启自旋-XX:+UseSpining(JDK6默认开启);自旋次数-XX:PreBlockSpin(10次)。

自适应自旋锁

1.“自适应”:自旋次数不固定,由前一次在同一锁对象的自旋时间及锁状态决定

2.自旋等待成功获取锁,且持有锁线程正在运行,那么自旋可以多等待相对更长时间;若是自旋很少能成功获得锁,则以后获取这个锁时可能直接省掉自旋过程。

锁消除

1.“锁消除”:即时编译在运行时,一些同步代码被检测到不存在共享数据竞争的锁,则进行锁消除

2.锁消除判定依据是逃逸分析数据支持(堆数据不会被其他线程访问)。

锁粗化

1.“锁粗化”:连续操作都对同一对象加锁,则把加锁同步的范围扩展(粗化)到整个操作的外部

2.适用:连续StringBuffer::append()、循环体中加锁。

轻量级锁

1.“轻量级锁”:两线程竞争同一把锁;两条以上线程竞争同一把锁,则轻量级锁(非阻塞同步 _ CAS)膨胀为重量级锁(互斥同步);

2.轻量级加锁工作过程:

   step1:程序进入同步代码块时,判定对象是否被锁定(锁标志位01状态);

   step2:没有被锁定,则在当前栈帧中创建锁记录(Lock Record)空间,用于存储锁对象目前的“Mark Word”的拷贝

   step3:CAS操作把对象的“Mark Word”更新为锁记录(Lock Record)空间地址

   step4:更新成功则当前线程加锁成功,且锁标志位改为00状态;

               更新失败(说明至少存在另一线程产生相互竞争),首先检查“Mark Word”是否指向当前线程的栈帧,若是则说明当前已经持有锁,直接进入代码块;否则被其他线程线程已加锁

   step5:存在两条以上线程竞争加锁,则轻量级锁膨胀为重量级锁(锁标志位10状态),后续线程进入阻塞状态。

偏向锁

1.“偏向锁”:锁无竞争的情况下把整个同步消除掉,即:持有偏向锁的线程无需同步操作

2.进入偏向锁模式:锁标志位01状态 + 偏向模式设置为1;一旦有其他线程尝试获取锁,则:偏向模式结束、锁定对象是否处于锁定状态决定是否撤销偏向锁(偏向模式设置为0)、标志位转为01(未锁定)或00(轻量级锁)状态;

3.JDK6启用偏向锁-XX:+UseBiasedLocking。

        偏向锁、轻量级锁的状态转换及对象Mark Word的关系,如下图所示。

五、参考资料

Java线程<一> _ 介绍_爱我所爱0505的博客-CSDN博客

volatile与synchronized实现原理_synchronized底层是总线锁吗_爱我所爱0505的博客-CSDN博客

Java内存模型<一> _ 基础_爱我所爱0505的博客-CSDN博客

Java内存模型<二> _ volatile/synchronized/final内存语义_volitale final sync_爱我所爱0505的博客-CSDN博客

Java线程<三> _ 线程间通信_java 三个线程间通信_爱我所爱0505的博客-CSDN博客

Lock锁<一> _ 基础_在lock锁的队列中,什么时候前一个节点会唤醒后一个节点_爱我所爱0505的博客-CSDN博客

Lock锁<二> _ 重入锁/读写锁_读写锁可重入锁_爱我所爱0505的博客-CSDN博客

深入理解Java内存模型(一)——基础_Java_程晓明_InfoQ精选文章

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/903341.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

iconfont的使用方法 | 踩过的坑

据 iconfont官网,其使用方法是 拷贝项目下面生成的fontclass代码,挑选相应图标并获取类名。 注意,这个fontclass代码是涵盖了你所有要使用的图标 起因:部分icont图标生效了,部分图标无效。 问题:fontclas…

邮件开发信技巧大公开!

特别是对外贸企业或者跨境电商企业来讲,写邮件开发信仍是一个常用的手。通过邮件开发信,企业可以很快地获得精准客户,同时扩展业务,进行营销活动。但是做过邮件群发的人可能都会遇到类似问题,比如邮件到达率低、邮件回…

Python框架【url_for 函数、重定向、响应内容、自定义响应、模板介绍、模板的使用、过滤器介绍、Jinja模板自带过滤器】(二)

👏作者简介:大家好,我是爱敲代码的小王,CSDN博客博主,Python小白 📕系列专栏:python入门到实战、Python爬虫开发、Python办公自动化、Python数据分析、Python前后端开发 📧如果文章知识点有错误…

大数据课程K3——Spark的常用案例

文章作者邮箱:yugongshiyesina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 掌握Spark的常用案例——WordCount; ⚪ 掌握Spark的常用案例——求平均值; ⚪ 掌握Spark的常用案例——求最大值和最小值; ⚪ 掌握…

​8th参考文献:[8]许少辉.乡村振兴战略下传统村落文化旅游设计[M]北京:中国建筑出版传媒,2022.

​8th参考文献:[8]许少辉.乡村振兴战略下传统村落文化旅游设计[M]北京:中国建筑出版传媒,2022&…

根据源码,模拟实现 RabbitMQ - 内存数据管理(4)

目录 一、内存数据管理 1.1、需求分析 1.2、实现 MemoryDataCenter 类 1.2.1、ConcurrentHashMap 数据管理 1.2.2、封装交换机操作 1.2.3、封装队列操作 1.2.4、封装绑定操作 1.2.5、封装消息操作 1.2.6、封装未确认消息操作 1.2.7、封装恢复数据操作 一、内存数据管理…

机器学习深度学习——NLP实战(情感分析模型——数据集)

👨‍🎓作者简介:一位即将上大四,正专攻机器学习的保研er 🌌上期文章:机器学习&&深度学习——BERT(来自transformer的双向编码器表示) 📚订阅专栏:机器…

【数据结构】_7.二叉树

目录 1.树形结构 1.1 树的概念 1.2 树的相关概念 1.3 树的表示 1.4 树在实际中的应用—表示文件系统的目录树结构 ​编辑​2.二叉树 2.1 概念 2.2 特殊二叉树 2.3 二叉树的性质 2.4 二叉树的存储结构 2.4.1 顺序存储结构(数组存储结构) 2.4.2…

LeetCode--HOT100题(36)

目录 题目描述:146. LRU 缓存(中等)题目接口解题思路代码 PS: 题目描述:146. LRU 缓存(中等) 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: LRUCache…

引人关注的领域 ---- 信号稀疏表示

本篇文章是博主在人工智能等领域学习时,用于个人学习、研究或者欣赏使用,并基于博主对人工智能等领域的一些理解而记录的学习摘录和笔记,若有不当和侵权之处,指出后将会立即改正,还望谅解。文章分类在学习摘录和笔记专…

redis实战-缓存数据解决缓存与数据库数据一致性

缓存的定义 缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码。防止过高的数据访问猛冲系统,导致其操作线程无法及时处理信息而瘫痪,这在实际开发中对企业讲,对产品口碑,用户评价都是致命的;所以企业非常重视缓存…

基于蜉蝣算法优化的BP神经网络(预测应用) - 附代码

基于蜉蝣算法优化的BP神经网络(预测应用) - 附代码 文章目录 基于蜉蝣算法优化的BP神经网络(预测应用) - 附代码1.数据介绍2.蜉蝣优化BP神经网络2.1 BP神经网络参数设置2.2 蜉蝣算法应用 4.测试结果:5.Matlab代码 摘要…

MySQL8.0.26-Linux版安装

MySQL8.0.26-Linux版安装 1. 准备一台Linux服务器 云服务器或者虚拟机都可以; Linux的版本为 CentOS7; 2. 下载Linux版MySQL安装包 MySQL :: Download MySQL Community Server (Archived Versions) 3. 上传MySQL安装包 4. 创建目录,并解压 mkdir mysql ​ tar -xvf mysql-8…

网站老域名跳转到新域名有哪些方法?内网穿透内网主机让外网访问

在网站服务器变更及本地主机搭建时,我们经常会遇到老域名地址跳转到新URL的配置,一些朋友还会面对无公网IP让外网访问的问题。今天我们来了解下网站老域名跳转到新域名有哪些方法,以及如何通过内网穿透实现内网主机让外网访问。 网站老域名跳…

【Unity小技巧】Unity2D TileMap的探究(最简单,最全面的TileMap使用介绍)

文章目录 前言介绍一、TileMap简单的使用1、创建Unity工程2、Tilemap的使用2.1、导入素材图片2.2、切割图片2.3、创建画板2.4、创建瓦片2.5、创建网格2.6、在网格上刷瓦片2.7、解决瓦片没有占满格子的问题2.8、解决瓦片之间有缝隙的问题2.9、擦除瓦片2.10、区域瓦片绘制2.11、瓦…

适合上班族做的4个低门槛的副业兼职

对于大多数职场中人来说,如果没有在30岁之后获得晋升,获得更好的发展平台,可能就会感到工作缺乏足够的吸引力了。当我们只有一份工作的时候,就好比把鸡蛋放在一个篮子里,把自己的青春放在一家公司里。这也就好比单一的…

华盛顿大学Baker实验室率先设计出双稳态结构蛋白质

在蛋白质世界,“结构决定功能”是一条基本原则。因此,很多人可能认为,一个蛋白质就应该有一个唯一确定的结构,使得它能够去执行确定的生物学功能。其实,在真实的世界中,蛋白质大多都是处于一种不断起伏的动…

MongDB【CRUD练习-条件查询-文档关系】

练习1-CRUD // 进入test数据库 use test; // 查询文档内容 db.students.find(); // 显示当前数据库中所有集合 show collections; // 向数据库的user集合中插入一个文档 db.users.insertOne({username: "lyh"} ); // 查看当前数据库中所有的集合 发现users集合被创建…

信号处理--基于EEG脑电信号的眼睛状态的分析

本实验为生物信息学专题设计小项目。项目目的是通过提供的14导联EEG 脑电信号,实现对于人体睁眼和闭眼两个状态的数据分类分析。每个脑电信号的时长大约为117秒。 目录 加载相关的库函数 读取脑电信号数据并查看数据的属性 绘制脑电多通道连接矩阵 绘制两类数据…

《强化学习:原理与Python实战》——可曾听闻RLHF

前言: RLHF(Reinforcement Learning with Human Feedback,人类反馈强化学习)是一种基于强化学习的算法,通过结合人类专家的知识和经验来优化智能体的学习效果。它不仅考虑智能体的行为奖励,还融合了人类专家…