【Synchronized我可以讲半小时】

本文详细介绍了JVM中对象内存布局的三个区域,特别是对象头的结构,包括MarkWord和ClassPointer,以及它们在锁状态转换中的作用。讲解了无锁、偏向锁、轻量级锁和重量级锁的状态转换过程,强调了锁升级的单向性和自旋锁在优化中的应用。同时,分析了自适应自旋锁如何根据历史成功率调整自旋次数,以提高效率。

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。实例数据:存放类的属性数据信息,包括父类的属性信息;对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。所以虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Monitor监视器对象就是存在于每个Java对象的对象头Mark Word中,也就是存储的指针的指向,Synchronized锁便是通过这种方式获取锁的。Monitor可以把它理解为一个同步工具,它通常被描述为一个对象,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象就带了一把看不见的锁,可以叫做内部锁或者Monitor锁。Synchronized在JVM里的实现基于进入和退出Monitor对象来实现方法同步和代码块同步的。
在这里插入图片描述
对象头的最后两位存储了锁的标志位,01是初始状态,没加锁状态,对象头里存储的是对象本身的哈希码。01是偏向锁状态,存储的是当前占用对象的线程ID。00是轻量级锁状态,存储指向线程栈中锁记录的指针。10是重量级锁状态,存储的技术就是重量级锁的指针了。
在这里插入图片描述锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。JDK5引进的CAS自旋,JDK6开始又引入了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略,这些优化使得Synchronized性能极大提高。
锁是升级的过程是怎样的?MarkWord是怎么变化的?
1.无锁状态:首先,当对象没有被锁时,MarkWord记录着对象的哈希码,这个时候锁标志为为01,是否偏向为0。
2.偏向锁状态:现在几乎所有的锁都是可重入的,也就是说已经获得锁的线程,可以多次锁住/解锁监视对象,每次加锁/解锁都会涉及到一些CAS操作,CAS操作会延迟本地调用,为什么这么说呢?这要从SMP(对称多处理器)架构说起,所有的CPU会共享一条系统总线(BUS),靠此总线连接主内存,每个核都有自己的一级缓存,每个核相对于BUS对称分布。举个例子,我电脑是六核的,假设一个核是Core1,一个核是Core2,这二个核可能会同时把主存中某个位置的值Load到自己的一级缓存中。当Core1在自己的L1 Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效,也就是所谓的Cache命中缺失,一旦发现失效就会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信叫做“Cache一致性流量”。如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致性”,从这个层面来说,锁设计的终极目标便是减少Cache一致性流量。而CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,降低Cache一致性流量。
在这里插入图片描述
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID就可以了。具体的流程是这样的,第一步,检测Mark Word是否为可偏向状态,就是是否为偏向锁1,锁标识位为01。第二步,如果是可偏向状态,测试线程ID是不是当前线程ID。如果是,就直接执行同步代码块。第三步,如果测试线程ID不是当前线程ID,就通过CAS操作竞争锁,竞争成功,就把Mark Word的线程ID替换为当前线程ID。第四步,如果CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程,继续往下执行同步代码块。安全点是jvm为了保证在垃圾回收的过程中引用关系不会发生变化,设置的安全状态,在这个状态上会暂停所有线程工作。一般有循环的末尾,方法临返回前,调用方法的call指令后,可能抛异常的位置,这些位置都可以算是安全点。
3.轻量级锁状态:有多个线程竞争同一个锁,那么将在锁升级为轻量级锁。升级过程是,在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word的拷贝,拷贝对象头中的Mark Word复制到锁记录中,为什么升级为轻量锁时要把对象头里的Mark Word复制到线程栈的锁记录中呢?因为在申请对象锁时,需要以该值作为CAS的比较条件,同时在升级到重量级锁的时候,能通过这个比较判定是否在持有锁的过程中此锁被其他线程申请过,如果被其他线程申请了,则在释放锁的时候要唤醒被挂起的线程。拷贝成功后,虚拟机将使用CAS操作把对象Mark Word中锁记录空间更新为指向当前线程锁记录空间的指针,然后把锁记录空间里的owner指针指向object mark word,如果这个更新动作成功了,那么当前线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。如果这个更新操作失败了,虚拟机首先会检查对象Mark Word中的Lock Word是否指向当前线程的栈帧,如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,进入自旋,若自旋结束时仍未获得锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。
4.自旋锁:自旋锁不是一种锁状态,而是一种策略。线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。所以引入自旋锁,当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间。自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好。自旋的次数必须要有一个限度,如果自旋超过了定义的限度仍然没有获取到锁,就应该被挂起。但是这个限度不能固定,程序锁的状况是不可预估的,所以JDK1.6引入自适应的自旋锁,线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少,甚至省略掉自旋过程,以免浪费处理器资源。自旋一段时间成功获得锁,就表示在轻量级锁状态,否则轻量级锁膨胀为重量级锁。
5.重量级锁状态:将锁标志为置为10,将MarkWord中指针指向重量级的monitor,阻塞所有没有获取到锁的线程。Synchronized是通过对象内部的监视器锁(Monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的,操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要比较长的时间,这就是为什么Synchronized效率低的原因,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。

在这里插入图片描述

整个图片,歇歇眼,文章大多不换行,排版基本都是一块的,三千五百字,口速快的话,半个小时差不多可以讲完,这篇博文主要是针对面试口述的,备战面试。

synchronizedJava中的关键字,是一种同步锁,用于解决多线程并发访问共享资源时的数据不一致问题。它可以修饰不同的对象,作用范围和对象各有不同: - **修饰代码块**:被修饰的代码块称为同步语句块,作用范围是大括号 `{}` 括起来的代码,作用对象是调用这个代码块的对象。例如: ```java public class SyncExample { private Object lock = new Object(); public void method() { synchronized(lock) { // 同步代码块 // 这里的代码在同一时间只能被一个线程访问 } } } ``` - **修饰方法**:被修饰的方法称为同步方法,作用范围是整个方法,作用对象是调用这个方法的对象。示例如下: ```java public class SyncExample { public synchronized void method() { // 同步方法 // 同一时间只能有一个线程调用该方法 } } ``` - **修饰静态方法**:作用范围是整个静态方法,作用对象是这个类的所有对象。示例代码如下: ```java public class SyncExample { public static synchronized void staticMethod() { // 静态同步方法 // 同一时间只能有一个线程调用该静态方法 } } ``` - **修饰类**:作用范围是 `synchronized` 后面括号括起来的部分,作用对象是这个类的所有对象。例如: ```java public class SyncExample { public void method() { synchronized(SyncExample.class) { // 同步类 // 同一时间只能有一个线程访问该类的同步代码 } } } ``` 在锁的概念方面,有实例锁和全局锁之分。实例锁对应的是 `synchronized` 关键字,锁在某一个实例对象上,如果该类是单例,那么该锁也具有全局锁的概念;而类锁(全局锁)对应的是 `static synchronized`(或者是锁在该类的 `class` 或者 `classloader` 对象上),无论实例多少个对象,线程都共享该锁 [^2]。 在性能方面,在单线程或多线程竞争不激烈的场景下,可以启用偏向锁来提高性能。但在多线程竞争激烈的场景下,偏向锁的撤销和升级会带来一定的性能开销,可以考虑禁用偏向锁 [^3]。 对于初学者而言,学习 `synchronized` 关键字时可以参考以下资源: - 官方文档 Oracle Java Documentation:提供了 Java 语言和类库的详细文档,对 `synchronized` 关键字有详细的说明。 - The Java Tutorials:适合初学者学习 Java 的基础知识,包括线程同步和 `synchronized` 关键字的内容。 - 书籍《Effective Java》:介绍了 Java 编程的最佳实践和技巧,对线程同步和 `synchronized` 关键字的使用有相关建议。 - 《Java 核心技术》:详细解了 Java 语言的基础知识和高级特性,包含了线程同步和 `synchronized` 关键字的内容 [^3]。
评论 51
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java程序员廖志伟

赏我包辣条呗

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值