高并发之深度解析对象与Sync

现在大多数虚拟机都是64位的,本文介绍的也是以64位虚拟机为基础。

 

1、对象的内存分布

在主流Java虚拟机HotSpot里面,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding),如下图一

 如上图对象头又分为对象标记(markOop)和类元信息(KlassOop),类元信息存储的是指向该对象类元数据(klass)的首地址。 

 在64位系统中,对象头(Header)又分为:Mark Word和类型指针;Mark Word占8个字节,类型指针占8个字节,一共16个字节(换句话说,你写了一句 Object o = new Object(); 什么都没干,o初始就占16个字节;

Mark Word(对象标识):存放锁标记,GC年龄(4bit,最大年龄15),Hash码等等;如图二的mark word,64位8个字节

类元信息(Class Pointer)就是该类在元空间的那份实例(class)的地址(例如:0xf80001e5),唯一的一份(因为类只加载一次)

实例数据(Instance Data)存放类的属性(Field)数据信息,包括父类的属性信息,方法是不占用对象大小的,下面会有testHello()方法案例说明;

对齐数据:虚拟机要求对象起始地址必须是8个字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这部分内存按8字节补充对齐(按我的理解来说,就是为了更好的操作管理对象,方便通过C/C++来与硬件交互)

实操—查看对象的内存分布 

利用openjdk的jol工具看看对象的真实大小情况 , pom里面导入下面的依赖

        <dependency>

            <groupId>org.openjdk.jol</groupId>

            <artifactId>jol-core</artifactId>

            <version>0.16</version>

<!--            <scope>provided</scope>-->

        </dependency>

下面这个案例分析了一个对象的大小 

package com.example.sycn;

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

/**
 * @Author George
 * @Date 2022/11/26 21:06
 * @VDesc
 */
//@Slf4j
public class JOLDemo {

    public static void main(String[] args) {

//        System.out.println(VM.current().details());
//        System.out.println(VM.current().objectAlignment());

        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

//        Customer customer = new Customer();
//        System.out.println(ClassLayout.parseInstance(customer).toPrintable());

    }
}

class Customer{

    int id;
    boolean flag = false;
    String age;

    public void testHello(int id){
        System.out.println(id + "hello everybody, have a nice day!");
    }
}

运行上述代码可以看到一个空的对象mark word是8个字节,类元信息是4个字节,还有4个字节的对象填充,为什么上面说”Object o = new Object(); 什么都没干,o初始就占16个字节“,是因为虚拟机做了优化——压缩指针,你在虚拟机启动参数里面输入`-XX:-UseCompressedClassPointers`指令再启动上述代码,就可以看到没有使用压缩指针的Object对象头是16字节,当然生产的时候不要更改上面标红的虚拟机参数,使用压缩指针更节省内存。

terminal输入这个👇命令就可以看到Java虚拟机初始化时的参数设置信息等

java -XX:+PrintCommandLineFlags -version  

 由上面的信息对比结合图一,不难看出对象的对象头,实例数据,对象填充的内存占用,分布等情况

2、深度解析Sync 

为什么任何一个对象都可以成为一把锁?

因为Java中所有的对象都默认继承了超类Object,而Object在JVM源码(C/C++)层面关联了一个对象ObjectMonitor,所以每个对象“天生”都带着一个对象监视器,也就是每一个被锁住的对象都会和自己的Monitor关联起来。Monitor是由ObjectMonitor实现的,Monitor是可以和对象一起创建、销毁的,Monitor的本质就是依赖底层操作系统的Mutex Lock 实现就是我们电脑的管程技术,已来对 对象 进行上锁。 

ObjectMonitor中有几个关键属性配合Synchronized对 对象 进行上锁

_owner指向持有ObjectMonitor对象的线程
_WaitSet存放于wait状态的线程队列
_EntryList存放处于等待锁block状态的线程队列
_recursious锁的重入次数
_count用来记录该线程获取锁的次数

Monitor与Java对象以及线程是如何关联?

如果一个Java对象被线程锁住,则该Java对象的mark word字段中LockWord指向Monitor的起始地址;Monitor的Owner字段会存放拥有该对象锁的线程ID。

sync加解锁详情

如👇图三 

管程:Monitors,也叫监视器

是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看作一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。

锁的升级

如👇图四

 偏向锁:默认是开启的,可以通过命令查看`java -XX:+PrintFlagsInitial | grep BiasedLock*`JVM偏向锁的信息(把命令放入Git Bash Here窗口里面执行),通过命令我们还能看到偏向锁开启有4秒钟的延时,将偏向锁的延时加载时间设为0秒👇

-XX:BiasedLockingStartupDelay=0

 实操下面的程序,即new出一个对象,直接给它加上偏向锁,程序中的延时5秒,也可以通过上面的JVM指令替代;值得注意的是分割线====,之后另起了一个线程t1,如果是sync给对象加锁,其前面的54bit位替换成持有该对象的线程ID,也即该线程t1持有拥有该对象锁,其他线程无法获得该对象锁,除非线程t1执行完毕,再来竞争,当然t1持有该锁时,其它线程过来竞争,锁会升级为轻量级锁。

        try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        System.out.println("=============");
        new Thread(() -> {
            synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());

            }
        },"t1").start();

 偏向锁使用一种等到竞争出现才释放锁的机制,只有当其它线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行,可以想象成JVM的STW机制),同时检查持有偏向锁的线程是否还在执行;一个线程正在执行synchronized方法(处于同步块),他还没有执行完,其他线程来抢夺,该偏向锁会被取消掉并出现锁升级——升级为轻量级锁。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获取该轻量级锁。如图四,如果一个对象调用过hashCode()方法,那么这个对象就不能被设置为偏向锁状态了,而当一个对象当前正处于偏向锁状态,有收到需要计算其一致性hashCode请求时,他的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。(偏向锁在JDK15逐步废弃了👿👿👿)

轻量级锁: 

关闭偏向锁,直接获取轻量级锁,可以执行`-XX:-UseBiasedLocking`命令实现。轻量级锁就是并发程度不高,约有2个线程来同时竞争同一把锁,所以,一个线程持有锁,另一个线程通过CAS自旋尝试获取该轻量级锁,与偏向锁不同的是,偏向锁,偏向于某一个线程,执行同步代码块出来后,不需要经行锁擦除;而轻量级锁每次执行完之后都需要把锁释放,原理就是把MarkWord的前62位写上持有该对象的线程ID,JVM会在持有该对象的线程中创建一个名叫Lock Record的空间,用于存放该对象的HashCode,分代年龄等;等待该线程释放锁时,再把HashCode,分代年龄等对象的信息写回MarkWord。从上可知,轻量级锁开销比偏向锁要大。

 重量级锁:重量级锁是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。当线程执行到monitor enter指令时,会尝试获取对象所对象的Monitor所有权,如果获取到了,及获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其它线程无法获取到这个Monitor。在重量级锁的实现中对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类里有字段可以记录非加锁状态下的MarkWord,其中自然可以存储原来的hashCode。

锁升级完整流程图: 

                        本图来源于网络,在此感谢作者,再由梦神拼好的,感谢你们使我站在巨人的肩膀上学习🥳

注:synchronized和 static synchronized前者是对象锁,后者是类锁,属于不同的锁,a线程加对象锁,b线程加类锁,加锁不同,a、b线程不会产生竟态条件。 

 赠上快捷键与表情,真香😅

shift+F6快速修改名称,可以改整个的

Ctrl+Alt+B:查看方法调用具体去哪个实现类

Shift+Alt+up/down移动代码行

Ctrl+Alt+T 选择语句块if/try等等

Ctrl+Alt+M 快速生成方法

😀 😃 😄 😁 😆 😅 😂 🤣 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 🥺 😢 😭 😤 😠 😡 🤬 🤯 😳 🥵 🥶 😱 😨 😰 😥 😓 🤗 🤔 🤭 🤫 🤥 😶 😐 😑 😬 🙄 😯 😦 😧 😮 😲 🥱 😴 🤤 😪 😵 🤐 🥴 🤢 🤮 🤧 😷 🤒 🤕 🤑 🤠 😈 👿 👹 👺 🤡 💩 👻 💀 ☠️ 👽 👾 🤖 🎃 😺 😸 😹 😻 😼 😽 🙀 😿 😾

emoji手势和人物
👋 🤚 🖐 ✋ 🖖 👌 🤏 ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍 👎 ✊ 👊 🤛 🤜 👏 🙌 👐 🤲 🤝 🙏 ✍️ 💅 🤳 💪 🦾 🦵 🦿 🦶 👂 🦻 👃 🧠 🦷 🦴 👀 👁 👅 👄 💋 🩸 👦 👧 👨 👩 👴 👵 👶 👱 👮 👲 👳 👷 👸 💂 🎅 👰 👼 💆 💇 🙍 🙎 🙅 🙆 💁 🙋 🙇 🙌 🙏 👤 👥 🚶 🏃 👯 💃 👫 👬 👭 💏 💑 👪
emoji动植物
🙈 🙉 🙊 🐵 🐒 🐶 🐕 🐩 🐺 🐱 😺 😸 😹 😻 😼 😽 🙀 😿 😾 🐈🐯 🐅 🐆 🐴 🐎 🐮 🐂 🐃 🐄 🐷 🐖 🐗 🐽🐏 🐑 🐐 🐪 🐫 🐘 🐭 🐁 🐀 🐹 🐰 🐇 🐻 🐨 🐼 🐾 🐔 🐓 🦆 🦢 🦜 🦉 🐣 🐤 🐥 🐦 🐧 🐸 🐊 🐢 🐍 🐲 🐉 🐳 🐋 🐬 🐟 🐠 🐡 🐙 🐚 🐌 🐛 🐜 🐝 🐞 🦋
💐 🌸 💮 🌹 🌺 🌻 🌼 🌷 🌱 🌲 🌳 🌴 🌵 🌾 🌿 🍀 🍁 🍂 🍃 🌍 🌎 🌏 🌐 🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘 🌙 🌚 🌛 🌜 ☀ 🌝 🌞 ⭐ 🌟 🌠 ☁ ⛅ ☔ ⚡ ❄ 🔥 💧 🌊

最近特喜欢的一句话:正因为作出评论是无比简单的,简单到很容易使人盲目,盲目的觉得自己亲自上手去做也依然简单。——致自己,要在路上,不要临渊羡鱼,”向前进“!!! 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值