「 代码性能优化 」作为一名Java程序员,你真的了解 synchronized 吗?(二)

前言

文接上篇,本文将继续介绍 Synchronized,感兴趣的小伙伴继续跟博主一起讨论下。上一篇文章地址:「 代码性能优化 」作为一名Java程序员,你真的了解 synchronized 吗?(一)

更多、更体系化的内容请持续关注 《菜鸟程序员成长计划》之代码性能优化 专栏,您的 关注、点赞、收藏 都将是小编持续创作的动力!

一、synchronized锁的底层实现

在探讨synchronized锁的底层实现原理之前,我们先来了解下java对象在内存中的结构

1. 对象的内存布局

以64位虚拟机为例:

img

从上面的这张图里面可以看出,对象在内存中的结构主要包含以下几个部分:

  • 对象头
    • Mark Word(标记字段):关于锁的信息。对象的Mark Word部分占4个字节/8个字节,表示对象的锁状态(比如轻量级锁的标记位,偏向锁标记位),另外还可以用来配合GC分代年龄、存放该对象的hashCode等。
    • Klass Pointer(Class对象指针):Class对象指针的大小也是4个字节/8个字节,其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址。
    • 数组长度:如果对象是数组类型,占用4个字节/8个字节,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
  • Instance Data(对象实际数据):这里面包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节。
  • padding data(对齐):如果上面的数据所占用的空间不能被8整除,padding则占用空间凑齐使之能被8整除。被8整除在读取数据的时候会比较快.

2. 对象的创建过程

2.1. 检查类对象是否被实例化过

​ jvm要检查类A是否已经被加载到了内存,即类的符号引用是否已经在常量池中,并且检查这个符号引用代表的类是否已被加载、解析和初始化过的。如果还没有,需要先触发类的加载、解析、初始化。然后在堆上创建对象。

2.2. 为新生对象分配内存

2.3. 完成实例数据部分的初始化

内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB的话,这一个工作也可以提前至TLAB分配时进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

2.4. 完成对象头的填充

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

​ 在上面工作都完成之后,java程序开始调用方法完成初始复制和构造函数,所有的字段都为零值,这样一个真正可用的对象才算完全创建出来。

3. synchronized锁基于对象内存模型和对象创建过程的实现原理

对象头 是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark WordClass Metadata Address组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  //锁计数器
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

每一个锁都对应一个 monitor 对象,在HotSpot虚拟机中它是由 ObjectMonitor 实现的(C++实现)。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态.

ObjectMonitor中有两个队列 _WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表(每个等待锁的线程都会被封装 ObjectWaiter 对象),_owner 指向持有 ObjectMonitor 对象的线程。

1、当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合;

2、当线程获取到对象的 monitor 后,进入 _owner 区域,并把monitor中的 _owner变量设置为当前线程,同时 monitor 中的计数器 _count 加1;

3、若线程调用 wait() 方法,将释放当前持有的 monitor_owner 变量恢复为null,_count 自减1,同时该线程进入 _WaitSet 集合中等待被唤醒;

4、若当前线程执行完毕也将释放 monitor(锁) 并复位变量的值,以便其他线程进入获取 monitor(锁).

monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因.

二、JVM对synchronized的优化

上一篇文章中提到JVM对synchronized的优化机制中的一种:锁膨胀 ,这篇文章我们将继续介绍其它集中机制:锁消除锁粗化自旋锁

感兴趣的小伙伴可以回过头去阅读一下:「 代码性能优化 」作为一名Java程序员,你真的了解 synchronized 吗?(一)

1、锁消除

​ 先解释一个概念:JIT

​ Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行的特别频繁时,会把这些代码认定为“热点代码”(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机会把这些代码编译成本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT编译器,不是Java虚拟机内必须的部分)。

​ 锁消除即代码中不存在锁竞争的地方使用了synchronized,jvm会自动帮你优化掉。具体解释就是:只有一个线程会用到,不会引起多个线程竞争的就没必要加锁了。

实例


/**
 * 锁消除
 */
public class Demo1 {
    
    static Object object = new Object();
 
    public static void fun1(){
        Object o = new Object();
        synchronized(o){
            System.out.println("Hello World!");
        }
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Demo1.fun1();
            },String.valueOf(i)).start();
        }
    }
}

​ main函数中,每个线程都会创建一个对象,各线程都有自己的私有资源,并不会引起线程之间的竞争,相当于每个线程都有一把锁,所以synchronized的存在毫无意义,程序编译过程中,JIT即时编译器会无视它。

2、锁粗化

​ 锁粗化即JIT会将首尾相接,前后相邻且都是锁同一个对象的代码块,JIT即时编译器就会把这几个synchronized块合并为一个大块。通过扩大锁的范围,避免反复加锁和释放锁。比如下面fun2经过锁粗化优化之后就和fun1执行效率一样了。

实例

public class HelloWorld {
   public static void main(String[] args) throws Exception {
        
        String user = new String("小明");
        int count = 0;
        
        //调用func1
		long start1 = System.nanoTime();
        fun1(user,count);
        System.out.println( System.nanoTime() - start1);
        
        //调用func2
		long start2 = System.nanoTime();
        fun2(user,count);
        System.out.println( System.nanoTime() - start2);
    }
    
    private static void  fun1(String user,int count){
        for (int i = 0; i < 100; i++) {
            count++;
        }
    }
    
    private static void  fun2(String user,int count){
        for (int i = 0; i < 100; i++) {
            synchronized (user) {
                count++;
            }
        }
    }
    
}

执行结果:

5034
5007

3、自旋锁与自适应自旋锁

轻量级锁失败后,JVM虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

自旋锁:一般情况下,共享数据的锁定状态持续时间较短,切换线程影响程序执行效率,通过让线程执行循环等待锁的释放,不释放内存资源。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是这种机制也有缺点:如果锁被其他线程长时间占用,一直不释放资源,会带来许多的性能开销。

自适应自旋锁:自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的影响性能开销缺点。

总结

以上内容 从synchronized锁的底层实现JVM对synchronized的优化两方面介绍了 synchronized 的知识,请继续关注博主,接下里会继续就 synchronized 的用法 展开讨论。

下一篇主题

根据获取的锁分类来分析synchronized的用法

参考

1、深入Synchronized的实现原理与源码分析

2、java对象在内存中的结构(HotSpot虚拟机)


致谢

莫笑少年江湖梦,谁不少年梦江湖.

本篇内容参考自互联网及开源社区,感谢前人的经验、分享和付出,让我们可以有机会站在巨人的肩膀上眺望星辰大海!

公众号搜索「 重载 」“chóng zài” 并关注,不定期分享Java相关技术栈资料、后端硬核技术干货、实用笔试面试题。您的每次阅读、每个点赞、每条评论都会激励到博主,持续输出优质内容!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

大白吃饱了吗

请博主喝杯咖啡,有力继续码字!

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

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

打赏作者

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

抵扣说明:

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

余额充值