深入理解并发编程之synchronized的monitor与对象布局原理
文章目录
一、Synchronized回顾
Synchronized是Java的关键字,是重量级锁,可以用在方法和代码块上。如果在普通方法上加上Synchronized锁,则使用this锁; 在静态同步方法上,则使用当前类的class字节码;也可以自定义锁的对象。
Synchronized底层是使用C++ 写的。
二、Synchronized关键字详解
1.以汇编的角度分析Synchronized
先看下面一段代码:
public class Test005 extends Thread {
private Object lockObject = new Object();
@Override
public void run() {
a();
}
public void a() {
// 虚拟机里面
synchronized (lockObject) {
System.out.println("我是A调用B");
// 当前线程变为阻塞状态同时 释放锁
lockObject.notify();
b();
}
}
private void b() {
synchronized (lockObject) {
System.out.println("我是B");
}
}
public static void main(String[] args) {
}
}
先把上面代码编译为class文件,然后使用javap反汇编为汇编代码
javap -p -v .\Test005.class
下面是反汇编出来的部分代码a()方法,b()方法的反汇编跟a差不多就不看了
可以看到当我们加了synchronized关键字后,汇编出来的代码多了两个操作monitorenter与monitorexit。由此可以得出这就是synchronized关键字实现的核心。
monitorenter详解
下面看一下Java的官方介绍:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
翻译过来就是这样的:
每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:
- 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
- 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
- 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
monitorexit详解
具体官方链接不上了,就在上个链接的下面
翻译过来就是这样的:
- 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
- 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取。
回头看上面的汇编截图,是不是看到两个monitorexit指令,这是为啥呢?
其实是这样的第一个monitorexit是正常退出,而第二个monitorexit代表的是异常退出,可以看到第二个下面紧跟着throw以及一个异常表。
monitor简介
monitor才是真正的锁,是一个c++对象拥有两个重要的属性:
- owner:拥有这把锁的线程,也就是当前锁标记持有者。
- recursions:记录线程拥有锁的次数,也就是重入锁重入的次数。按照上面的demo可以这样理解每次执行monitorenter都会+1,a方法调用了b方法,这是一个重入锁,当进入a方法的时候recursions由0变为1,当由a再进入b的时候recursions由1变成了2;而每次执行monitorexit都会-1,由b退出的时候recursions变成了1,由a退出的时候recursions变成了0,这个时候代表这个重入锁完全退出了,可以正常释放锁标记被其他线程去争夺了。
2.以c++的角度分析monitor
monitor属性分析
monitor是由c++写的,我们需要使用虚拟机来查看其源码,下面是虚拟机下载地址:
下载下来之后在Windows文件管理搜索objectmonitor关键字就会找到:
打开这个文件看一下monitor有下面属性:
上面解释了synchronized是重入锁的原因,下面根据属性说一下synchronized为非公平锁的原因,_cxq是当没有线程持有锁标记的时候形成的单项链表排队,看着是公平,但是这里每次释放锁后都重新生成,也就是说每次释放锁都会重新排队排第一位的持有锁标记(每次释放锁_EntryList里面所有阻塞的线程重新生成_cxq)。
三、java的对象布局
1.java对象布局
在JVM中,对象在内存中的布局分为三个部分:对象头、实例数据和对齐填充。
对象头包含Mark Word与Klass Pointer
Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
Klass Pointer:对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 )
64位对象头Mark Word占位图:
上虚拟机源码(只看64位的):
详细代码查看对象占用内存的大小,需要引入jol-core:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
引入后看下面代码:
public class MeiteLock {
private int userId1; // 4个字节
public static void main(String[] args) {
MeiteLock meiteLock = new MeiteLock();
System.out.println(meiteLock.hashCode());
System.out.println(Integer.toHexString(meiteLock.hashCode())); //16进制hashcode
System.out.println(ClassLayout.parseInstance(meiteLock).toPrintable()); //以表格的形式打印
}
}
对象头占了16个字节,实例数据int的uid占了4个字节,还有4个字节的自动填充数据(必须是8的倍数,不足的需要填充,仅仅是占位符),总共24个字节。黄色的是对象布局里面的hashcode,注意倒着看的,参照Mark Word布局图:第一个字节01是锁标志位+偏向锁+分代年龄+cms_free,然后是hashcode,再就是unused。
针对自动填充数据下面我们再加个int属性uid2,这时候正好满足8的倍数,就不需要填充了。
这里需要注意的是虚拟机会开启自动指针压缩,需要去掉自动指针压缩
-XX:-UseCompressedOops
2.基本数据类型占用字节
- bit –位:位是计算机中存储数据的最小单位,指二进制数中的一个位数,其值为“0”或“1”。
- byte –字节:字节是计算机存储容量的基本单位,一个字节由8位二进制数组成。在计算机内部,一个字节可以表示一个数据,也可以表示一个英文字母,两个字节可以表示一个汉字。
1Byte=8bit (1B=8bit)
1KB=1024Byte(字节)=8*1024bit
1MB=1024KB
1GB=1024MB
1TB=1024GB
类型 | 字节 |
---|---|
int | 4 byte |
short | 2 byte |
long | 8 byte |
byte | 1 byte |
char | 2 byte |
float | 4 byte |
double | 8 byte |
boolean | 1 byte |
内容来源: 蚂蚁课堂
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/3406.html