public static void main(String[] args) {
// 锁作用于代码块
synchronized (lock) {
System.out.println(“hello word”);
}
}
// 锁作用于方法
public synchronized void test() {
System.out.println(“test”);
}
}
将该代码进行编译后,查看其字节码,核心代码如下:
{
public com.joonwhee.SynchronizedDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object.“”😦)V
4: return
LineNumberTable:
line 9: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter // 进入同步块
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #4 // String hello word
11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: aload_1
15: monitorexit // 退出同步块
16: goto 24
19: astore_2
20: aload_1
21: monitorexit // 退出同步块
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // ACC_SYNCHRONIZED 标记
Code:
stack=2, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String test
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 20: 0
line 21: 8
}
synchronized 修饰代码块时,编译后会生成 monitorenter 和 monitorexit 指令,分别对应进入同步块和退出同步块。可以看到有两个 monitorexit,这是因为编译时 JVM 为代码块添加了隐式的 try-finally,在 finally 中进行了锁释放,这也是为什么 synchronized 不需要手动释放锁的原因。
synchronized 修饰方法时,编译后会生成 ACC_SYNCHRONIZED 标记,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了则会先尝试获得锁。
两种实现其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
22、介绍下 Mark Word?
在介绍 Mark Word 之前,需要先了解对象的内存布局。 HotSpot 中,对象在堆内存中的存储布局可以分为三部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
1)对象头(Header)
主要包含两类信息:Mark Word 和 类型指针。
Mark Word 记录了对象的运行时数据,例如:HashCode、GC分代年龄、偏向标记、锁标记、偏向的线程ID、偏向纪元(epoch)等,32 位的 markword 如下图所示。
类型指针,指向它的类型元数据的指针,Java 虚拟机通过这个指针来确定该对象时哪个类的实例。如果对象是数组,则需要有一个用于记录数组长度的数据。
2)实例数据(Instance Data)
对象存储的真正有效信息,即我们在代码里定义的各种类型的字段内容。
3)对齐填充(Padding)
Hotspot 要求对象的大小必须是8字节的整数倍,因此,如果实例数据不是8字节的整数倍时,需要通过该字段进行填充。
23**、介绍下 Lock Record?**
锁记录,这个大家应该都听过,用于轻量级锁时暂存对象的 markword。
Lock Record 在源码中为 BasicObjectLock,源码如下:
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
private:
BasicLock _lock;
oop _obj;
};
class BasicLock VALUE_OBJ_CLASS_SPEC {
private:
volatile markOop _displaced_header;
};
其实就两个属性:
1)_displaced_header:用于轻量级锁中暂存锁对象的 markword,也称为 displaced mark word。
2)_obj:指向锁对象。
Lock Record 除了用于暂存 markword 之外,还有一个重要的功能是用于实现锁重入的计数器,当每次锁重入时,会用一个 Lock Record 来记录,但是此时 _displaced_header 为 null。
这样在解锁的时候,每解锁1次,就移除1个 Lock Record。移除时,判断 _displaced_header 是否为 null。如果是,则代表是锁重入,则不会执行真正的解锁;否则,代表这是最后一个 Lock Record,此时会真正执行解锁操作。
24、什么匿名偏向?
所谓的匿名偏向是指该锁从未被获取过,也就是第一次偏向,此时的特点是锁对象 markword 的线程 ID 为0。
当第一个线程获取偏向锁后,线程ID会从0修改为该线程的 ID,之后该线程 ID 就不会为0了,因为释放偏向锁不会修改线程 ID。
这也是为什么说偏向锁适用于:只有一个线程获取锁的场景。
25、偏向锁模式下 hashCode 存放在哪里?
偏向锁状态下是没有地方存放 hashCode 的。
因此,当一个对象已经计算过 hashCode 之后,就再也无法进入偏向锁状态了。
如果一个对象当前正处于偏向锁状态,收到需要计算其 hashCode 的请求时(Object::hashCode()或者System::identityHashCode(Object)方法的调用),它的偏向锁状态就会立即被撤销。
26、偏向锁流程?
首先,在开启偏向锁的时候,对象创建后,其偏向锁标记位为1。如果没开启偏向锁,对象创建后,其偏向锁标记位为0。
加锁流程:
1)从当前线程的栈帧中寻找一个空闲的 Lock Record,将 obj 属性指向当前锁对象。
2)获取偏向锁时,会先进行各种判断,如加锁流程图所示,最终只有两种场景能尝试获取锁:匿名偏向、批量重偏向。
3)使用 CAS 尝试将自己的线程 ID 填充到锁对象 markword 里,修改成功则获取到锁。
4)如果不是步骤2的两种场景,或者 CAS 修改失败,则会撤销偏向锁,并升级为轻量级锁。
5)如果线程成功获取偏向锁,之后每次进入该同步块时,只需要简单的判断锁对象 markword 里的线程ID是否自己,如果是则直接进入,几乎没有额外开销。
解锁流程:
偏向锁的解锁很简单,就是将 obj 属性赋值为 null,这边很重要的点是不会将锁对象 markword 的线程ID还原回0。
偏向锁流程中,markword 的状态变化如下图所示:
27、批量重偏向、批量撤销?启发式算法?
上面我们提到了批量重偏向,与批量重偏向同时被引入的还有批量撤销,官方统称两者为 “启发式算法”。
为什么引入启发式算法?
从上面的介绍我们知道,当只有一个线程获取锁时,偏向锁只需在第一次进入同步块时执行一次 CAS 操作,之后每次进入只需要简单的判断即可,此时的开销基本可以忽略。因此在只有一个线程获取锁的场景中,偏向锁的性能提升是非常可观的。
但是如果有其他线程尝试获得锁时,此时需要将偏向锁撤销为无锁状态或者升级为轻量级锁。偏向锁的撤销是有一定成本的,如果我们的使用场景存在多线程竞争导致大量偏向锁撤销,那偏向锁反而会导致性能下降。
JVM 开发人员通过分析得出以下两个观点:
观点1:对于某些对象,偏向锁显然是无益的。例如涉及两个或更多线程的生产者-消费者队列。这样的对象必然有锁竞争,而且在程序执行过程中可能会分配许多这样的对象。
该观点描述的是锁竞争比较多的场景,对这种场景,一种简单粗暴的方法是直接禁用偏向锁,但是这种方式并不是最优的。
因为在整个服务中,可能只有一小部分是这种场景,因为这一小部分场景而直接放弃偏向锁的优化,显然是不划算的。最理想的情况下是能够识别这样的对象,并只为它们禁用偏向锁。
批量撤销就是对该场景的优化。
观点2:在某些情况下,将一组对象重新偏向另一个线程是有好处的。特别是当一个线程分配了许多对象并对每个对象执行了初始同步操作,但另一个线程对它们执行了后续工作。
我们知道偏向锁的设计初衷是用于只有一个线程获取锁的场景。该观点中后半部分其实是符合这场场景的,但是由于前半部分而导致不能享受偏向锁带来的好处,因此 JVM 开发人员要做的就是识别出这场场景,并进行优化。
对于这种场景,官方引入了批量重偏向来进行优化。
批量重偏向
JVM 选择以 class 为粒度,为每一个 class 维护了一个偏向锁撤销计数器。每当该 class 的对象发生偏向锁撤销的时候,计数器值+1。
当计数器的值超过批量重偏向的阈值(默认20)的时候,JVM 认为此时命中了上述的场景2,就会对整个 class 进行批量重偏向。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
最后
终极手撕架构师的学习笔记:分布式+微服务+开源框架+性能优化
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
12260244541)]
最后
终极手撕架构师的学习笔记:分布式+微服务+开源框架+性能优化
[外链图片转存中…(img-ZtC3ZnFR-1712260244541)]
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算