JVM中的锁(中):锁膨胀和锁消除

本文详细介绍了JVM中的锁机制,从自旋锁到重量级锁的膨胀过程。当轻量级锁加锁失败,经过自旋锁尝试后,会升级为重量级锁,利用mutexlock实现互斥。此外,还提到了锁消除技术,通过逃逸分析来优化性能,减少不必要的同步操作。

目录

锁膨胀

自旋锁

重量级锁

锁消除


      接着上一篇,偏向锁失效后JVM让线程去申请轻量级锁,轻量级锁就是一种乐观思想,举个例子,在写入数据时会先判断这个期间有没有其他线程修改过这个数据,具体方式是读出版本号or时间戳,如果没有,就可以加锁,进行写入修改,否则继续重复读出数据,比较,再写入,轻量级锁就是这么个思想,使用的也是CAS操作。如果轻量级锁获取失败,证明有多个线程在同时竞争这个对象,这时候临界区线程竞争激烈,如果想要保证数据完整性,就要采用更重量级别的锁,从偏向锁到轻量级锁,再到后面的重量级锁锁,都是一个锁膨胀过程。

 

锁膨胀

      锁膨胀过程我打算还是回到偏向锁开始,当一个线程成功进入临界区访问资源时,会在自己的Java对象头中Mark Word中记录线程的id,让锁处于偏向状态,偏向该线程,这样当下一次有线程试图获取锁时,JVM先检查对象头中Mark Word里是否存有该线程的id,如果有,则直接进入,不需要任何同步操作。如果Mark Word中没有该线程的id,那么就要分两种情况判断,如果偏向锁标志位还是为1,证明偏向锁仍然有效,并没有多个线程同时竞争,只是当前访问临界区的线程不是之前偏向的线程,这时只要用一次CAS操作将对象头Mark Word中线程id指向当前请求的线程计科。如果偏向锁标志位为0,说明偏向锁已经失效,有多个线程进行了竞争,锁已经膨胀为轻量级锁。轻量级锁加锁时通过CAS操作尝试修改Java对象头,首先在Mark Word中保存当前线程的指针,然后将lock标志位修改为00;解锁则将biased_lock标志位设置为0,lock标志位设置为01,如果加锁或设置无锁状态时CAS操作失败,具体体现在线程进行CAS操作时,期待的值不是原本的Mark Word,而是一个指针,如果该指针指向当前线程,则说明这个线程已经获得了该锁,否则表明发生了竞争,锁膨胀为重量级锁。

自旋锁

      不过,在膨胀为重量级锁前,JVM还会做一些挣扎,因为线程阻塞后再被唤醒对性能消耗还是挺大的,为了减少大量线程频繁上下文切换的情况,在线程锁膨胀前,会做几轮空循环,看看能不能尽快进入临界区,如果做了几轮空循环(也就是自旋)后,线程能够成功获得锁,那么线程就可以继续执行,否则几轮自旋操作后还没能获得锁,线程就真的被挂起了。通常自旋的次数为10次,

      自旋锁的目的是希望线程能尽快获得锁,减少程序中线程阻塞的次数,但是否能优化性能是要分场景的,对于多线程占用锁时长很短的场景下,一个线程自旋后能够获得锁的几率很大,的确能大大减少线程被挂起的次数,让整个程序执行的更加连贯。但是,如果在多线程需要对数据进行长时间操作的场进下,因为线程占用锁的时间较长,即使其他线程进行自旋等待,也可能无法获得锁,这样就会浪费占用的CPU,最后还是要被挂起。

重量级锁

      当线程尝试多次申请轻量级锁失败,且自旋也未能获得锁后,就会膨胀成为重量级锁,重量级锁就是用mutexlock来实现的互斥锁,是一种悲观锁,即悲观地认为每次操作数据时都会被其他线程修改,以防万一就都先上锁,这样别的线程试图访问这一临界区资源时就会阻塞,直到获得锁。锁升级为重量级锁把Mark Word中的lock标志位设置为10,biased_lock标志位为0,JVM检查到锁是重量级锁后,会把线程阻塞,等其他线程释放锁后,再唤醒阻塞的线程。

      总的来说从偏向锁膨胀到重量级锁的过程如上图所示。

 

锁消除

      我们知道Java程序是一边执行一边编译的,在JVM即时编译过程中会去扫描这些代码,做逃逸分析,看看那些对象是线程安全的,那么对于这些对象的访问就不用做同步操作,减少锁带来的性能损失。逃逸分析具体有对线程的逃逸分析和对方法的逃逸分析,拿方法逃逸举例,JVM检测某个对象在方法中被定义之后,会不会作为参数被外部方法引用,如果不会,表示该对象是线程安全的,那对象不会逃逸到方法之外,那么对该变量的访问就可以不做同步操作。事实上JVM就是这么做,在程序中对于一些不存在竞争的代码段,减少同步操作可以提升执行速度,用逃逸分析来得出那些不存在竞争的代码段,然后消除一些锁操作。逃逸分析可以用参数-XX:+EscapeAnalysis来开启,然后参数-XX:+EliminateLocks来开启锁消除,来看一个例子:


public class EliminateLockDemo {
	
	public static void main(String[] args) {
		StringBuffer buffer = new StringBuffer();
		String s1, s2;
		
		long startTime = System.currentTimeMillis();
		for(int i=0; i<100000; i++) {
			s1 = "dali";
			s2 = "haitang";
			buffer.append(s1);
			buffer.append(s2);
			buffer.toString();
		}
		long endTime = System.currentTimeMillis();
		System.out.println("耗时: " + (endTime-startTime) + "ms");
		
	}

}

      程序很简单,就是不停地拼接字符串,因为append()方法是同步操作,所以程序会产生大量加锁释放锁如果我们带有参数:

-XX:+DoEscapeAnalysis  –XX:-EliminateLocks

      来执行程序,也就是关闭了锁消除,执行时产生大量同步操作,耗费的时间也相对较多:

      而如果我们用–XX:+EliminateLocks打开锁消除,执行时间就有了较可观的提升:

 

<think>好的,我现在要处理用户关于JVM机制的问题。首先,我需要理解用户的具体需求。他们提到了JVM的原理实现,可能涉及synchronizedReentrantLock的区别,以及JVM内部的优化技术。 根据用户提供的引用资料,引用[1][2]提到了synchronizedReentrantLock的区别,包括隐式显式的不同,以及它们的应用范围(单体分布式)。引用[4]则详细介绍了JVM优化技术,如自旋消除膨胀等。这些应该是回答的重点。 首先,我需要整理JVM机制的基本分类,比如内置显式。然后,详细说明synchronized的实现原理,包括对象头、监视器等概念。接下来是ReentrantLock的底层机制,比如AQS(抽象队列同步器)。用户可能还关心两者的区别,所以需要对比它们的特性,如可中断性、公平性等。 然后,关于优化部分,需要逐一解释每种优化技术,比如自旋、适应性自旋、消除膨胀、轻量级偏向。这里要确保每个术语都有清晰的解释,并且结合引用[4]的内容进行说明。 在组织内容时,要注意用户要求的结构清晰,分步骤解答。可能还需要考虑生成相关的问题,比如优化的具体应用场景,或者synchronizedReentrantLock的性能差异。但根据用户的问题,他们主要关注原理实现,所以这部分需要详细展开。 需要检查是否有遗漏的信息,例如引用[3]提到的书籍可能提供JVM内部实现的背景,但用户提供的引用中没有直接相关的内容,所以可能不需要深入讨论。另外,确保所有技术术语使用正确,比如“轻量级“偏向”的实现步骤。 最后,按照要求使用正确的中文表达,数学公式用$...$格式,独立公式用$$...$$。但在这个问题中可能涉及较少数学公式,但如果有,比如算法步骤中的某些计算,需要正确应用格式。同时,引用标识要自然地添加到相关段落的末尾,确保每个引用对应正确的内容。 总结步骤: 1. 介绍JVM机制的分类,内置显式。 2. 详细讲解synchronized的实现,包括对象头、监视器、升级过程。 3. 解释ReentrantLock的AQS机制其特点。 4. 对比两者的区别,如使用方式、功能特性等。 5. 分点说明JVM优化技术,引用[4]中的内容。 6. 生成相关问题,覆盖原理、优化、应用场景等。 需要确保每个部分的信息准确,引用正确。例如,在讲synchronized的升级时,引用[4]中的轻量级偏向;在对比类型时,引用[1][2]关于单体显式的内容。 可能遇到的疑问点:膨胀的具体过程是怎样的?需要确保描述准确。另外,适应性自旋普通自旋的区别是什么?需要明确说明自适应是根据历史等待时间调整自旋次数。 还要注意用户提供的引用中是否有冲突或需要进一步解释的地方。例如,引用[4]提到了多个优化技术,需要逐一解释清楚,避免混淆。 完成内容后,检查是否符合用户的结构要求,使用中文,避免使用Markdown,正确使用引用标识,并生成相关问题。</think>### JVM机制原理与实现分步解析 #### 一、JVM机制分类 JVM机制可分为**内置**(如$synchronized$)**显式**(如$ReentrantLock$)两类: 1. **内置** - 通过$synchronized$关键字实现,依赖JVM底层机制自动管理的获取与释放[^1] - 示例:`synchronized(obj) { ... }` 2. **显式** - 需通过$ReentrantLock$等API手动调用$lock()$$unlock()$方法[^1] - 示例:`Lock lock = new ReentrantLock(); lock.lock(); try{...} finally{lock.unlock();}` #### 二、synchronized实现原理 1. **对象头结构** 每个Java对象头部包含**Mark Word**字段,存储状态信息: - 无状态:记录哈希码、分代年龄 - 偏向:记录线程IDepoch值 - 轻量级:指向栈中记录的指针 - 重量级:指向互斥量(mutex)的指针 2. **升级过程** $$无 \rightarrow 偏向 \rightarrow 轻量级 \rightarrow 重量级$$ - **偏向**:当同一线程多次访问时直接进入,无需CAS操作[^4] - **轻量级**:通过CAS自旋尝试获取,减少线程阻塞 - **重量级**:竞争激烈时升级为操作系统级别的互斥 #### 三、ReentrantLock实现机制 1. **AQS(AbstractQueuedSynchronizer)** 核心组件为双向CLH队列,通过$volatile$变量$state$表示状态: ```java // 获取伪代码 while(true) { if(compareAndSetState(0,1)) { // CAS操作 setExclusiveOwnerThread(currentThread); break; } enqueue(currentThread); // 加入等待队列 } ``` 2. **可重入性** 通过记录当前持有的线程重入次数实现,如$state$字段递增 #### 四、关键差异对比 | 特性 | synchronized | ReentrantLock | |---------------------|---------------------------------------|-----------------------------| | **获取方式** | JVM自动管理 | 手动lock/unlock[^1] | | **可中断性** | 不支持 | 支持lockInterruptibly() | | **公平性** | 非公平 | 可配置公平/非公平 | | **条件变量** | 只能绑定一个条件 | 支持多个Condition对象 | #### 五、JVM优化技术[^4] 1. **自旋与适应性自旋** - 传统自旋:固定次数(如10次)循环尝试获取 - 适应性自旋:根据前一次自旋成功概率动态调整次数 2. **消除(Lock Elimination)** JIT编译器通过逃逸分析,消除不可能存在共享资源竞争的 ```java public String concat(String s1, String s2) { StringBuffer sb = new StringBuffer(); // 局部变量,消除 sb.append(s1); sb.append(s2); return sb.toString(); } ``` 3. **膨胀(Lock Coarsening)** 将相邻的同步代码块合并,减少获取/释放次数: ```java // 优化前 synchronized(obj) { doA(); } synchronized(obj) { doB(); } // 优化后 synchronized(obj) { doA(); doB(); } ``` 4. **轻量级(Lightweight Locking)** 通过CAS操作将Mark Word复制到线程栈帧,避免直接使用重量级 5. **偏向(Biased Locking)** 首次获取后记录线程ID,后续无需同步操作,适用于单线程重复访问场景 #### 六、应用场景选择建议 - **synchronized适用场景**:简单同步需求、追求代码简洁性 - **ReentrantLock适用场景**:需要可中断、超时获取、公平等高级特性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值