volitile和synchronized的应用和实现原理

本文深入探讨Java并发机制的底层实现原理,包括volatile关键字的作用机制、synchronized锁的不同状态及其转换过程,以及这些机制如何帮助解决多线程环境下的数据一致性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java并发机制的底层原理实现

1. 简要

  java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM中,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,java中所使用的并发机制依赖于JVM的实现和CPU的指令

2. volatile的应用

2.1 定义:

java语言规范第3版中对volitile的定义如下:

  java编程语言允许线程访问共享变量。为了确保共享变量能被准确和一致的更新,线程应该确保通过其他排他锁单独获得这个变量。

2.2 我理解的volitile实现原理:

  当对一个被volitile声明的变量进行回写操作时,CPU会立即将这个变量所在的缓存行回写到内存中。但是,如果仅仅时这样的话,其他的CPU中缓存了该数据的缓存行仍然是旧值,如果其他CPU仍然对旧值进行操作,那么就会出现一致性的问题。因此,在这个基础上,还要增加一个缓存一致性协议:每个CPU通过嗅探总线上传播的数据来检查自己缓存行的数据是否过期,如果发现自己缓存行对应的内存地址发生了改变,就会将该缓存行中的数据置为无效。当处理器需要对这个数据进行处理时,必须从内存中重新获取这个数据,从而保证对数据操作的一致性。

(这里用到的机制是缓存锁机制,也就是说,当CPU把数据回写到内存时,不会回写到之前的存储这个数据的内存地址,而是换个内存地址存,当其他CPU发现自己缓存行中数据的内存地址发生改变时,就会使该缓存行失效)

2.3 详解

和实现原理相关的CPU术语与说明:

在这里插入图片描述

volatile是如何来保证可见性的呢?让我们在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情。
Java代码如下。

instance = new Singleton(); // instance是volatile变量

转变成汇编代码,如下。

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码

lock addl $0×0,(%esp);

通过查IA-32架构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发了两件事情。

1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

而具体是怎么导致其他处理器的缓存无效呢?

  IA-32处理器和Intel 64处理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如,在Pentium和P6 family处理器中,如果通过嗅探一个处理器检测到了其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

  总而言之,就是就是通过执行这两件事情,保证并发编程中volitile修饰变量的一致性

3 synchronized的应用

synchronized是java并发编程中的元老级任务,也称为重量级锁。

3.1 synchronized实现同步的基础:

java中的每个对象都可以作为锁,具体表现为:

  • 对于普通同步的方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的Class对象
  • 对于同步方法块,锁是synchronized括号里配置的对象

  当一个线程想要进入同步代码块时,就必须先得到对象的锁,退出或者抛出异常时必须释放对象的锁。而锁到底是个什么东西,它存储着什么信息?,它又存在哪里?

  从JVM规范中可以看到Synchronized在JVM中的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步的,但两者的实现细节不一样。代码块同步是使用 m o n i t o r e n t e r monitorenter monitorenter m o n i t o r e x i t monitorexit monitorexit两个指令实现的,而方法同步是用其他方法实现的,规范中并没有详细说明。

   m o n i t o r e n t e r monitorenter monitorenter指令在编译后插入到同步代码块的开始位置, m o n i t o r e x i t monitorexit monitorexit插入到方法结束处和异常处。这两个指令必须是一对出现。任何一个对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,也就是尝试获取对象的锁!详细可以参考博客Java并发编程:Synchronized及其实现原理

3.1.1 java对象头

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

Header的结构

长度内容说明
32/64bitMark Word存储对象的hashCode或锁信息
32/64bitClass MetaDataAddress存储在对象类型数据的指针
32/64bitArray length数组的长度(如果该对象是数组的话)

  synchronized用到的锁是存入到Java对象头中的,HotSpot虚拟机的对象头(Object Header)包括两到三个部分的存储信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。第二部分是类型指针,即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。

  也就是说,每个对象都会有2-3个字宽来存储对象头(非数组对象2个,数组对象三个)

  对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额 外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示:

img

3.2锁的状态

  锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。

3.2.1 轻量级锁
3.2.1.1 轻量级锁的加锁过程
  1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图2.1所示。
  2. 拷贝对象头中的Mark Word复制到锁记录中。
  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word(Mark Word中增加指向Lock record的指针,Lock record中的owner为指向该锁对象的Mark Word地址的指针)。如果更新成功,则执行步骤(4),否则执行步骤(5)。
  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2.2所示。
  5. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则会自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

img点击并拖拽以移动
​ 图2.1 轻量级锁CAS操作之前堆栈与对象的状态

在这里插入图片描述
​ 图2.2 轻量级锁CAS操作之后堆栈与对象的状态

3.2.1.2 轻量级锁解锁解锁过程
  1. 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
  2. 如果替换成功,整个同步过程就完成了。
  3. 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
3.2.2 偏向锁

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

3.2.2.1 偏向锁获取过程:

(1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

(2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。

(3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

(5)执行同步代码。

3.2.2.2 偏向锁的释放:

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

举例:线程1获取了偏向锁,线程2也来竞争锁

  1. 线程2来竞争锁对象;
  2. 判断当前对象头是否是偏向锁;
  3. 判断拥有偏向锁的线程1是否还存在;
  4. 线程1不存在,直接设置偏向锁标识为0(线程1执行完毕后,不会主动去释放偏向锁);
  5. 使用cas替换偏向锁线程ID为线程2,锁不升级,仍为偏向锁;
  6. 线程1仍然存在,暂停线程1;
  7. 设置锁标志位为00(变为轻量级锁),偏向锁为0;
  8. 从线程1的空闲monitor record中读取一条,放至线程1的当前monitor record中;
  9. 更新mark word,将mark word指向线程1中monitor record的指针;
  10. 继续执行线程1的代码;
  11. 锁升级为轻量级锁;
  12. 线程2自旋来获取锁对象;
3.2.3重量级锁

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

3.2.4重量级锁、轻量级锁和偏向锁之间转换
  1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于**偏向锁 **

  2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1

  3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。

  4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁

    如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

  5. 如果自旋成功则依然处于轻量级状态。

  6. 如果自旋超过阈值次数,失败,则升级为重量级锁。

img点击并拖拽以移动
该图主要是对上述内容的总结,如果对上述内容有较好的了解的话,该图应该很容易看懂。

3.2.5三重类型锁的对比

在这里插入图片描述

  1. Synchronized锁的到底是什么, 锁住的是代码还是对象)(答案锁的是对象)?

  2. java中锁,锁的是对象,它是怎么实现的?

    答案:通过获取monitor对象的所有权

搞明白这两个问题,就好懂了

参考资料:

Synchronized与三种锁态

Java并发编程:Synchronized及其实现原理

Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

http://www.cnblogs.com/lingepeiyong/archive/2012/10/30/2745973.html

【死磕Java并发】—–深入分析synchronized的实现原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值