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

简介: 文接上篇,本文将继续介绍 Synchronized,感兴趣的小伙伴继续跟博主一起讨论下。您的 关注、点赞、收藏 都将是小编持续创作的动力!

前言

文接上篇,本文将继续介绍 Synchronized,感兴趣的小伙伴继续跟博主一起讨论下。您的 关注、点赞、收藏 都将是小编持续创作的动力!

一、synchronized锁的底层实现

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

1. 对象的内存布局

以64位虚拟机为例:

网络异常,图片无法展示
|

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

  • 对象头
  • 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程序开始调用<init>方法完成初始复制和构造函数,所有的字段都为零值,这样一个真正可用的对象才算完全创建出来。

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对象的线程,当多个线程同时访问一段同步代码时,首先会进入EntryList 集合,当线程获取到对象的monitor 后进入 _owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放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会自动帮你优化掉。具体解释就是:只有一个线程会用到,不会引起多个线程竞争的就没必要加锁了。

实例

 

/*** 锁消除*/publicclassDemo1 {
staticObjectobject=newObject();
publicstaticvoidfun1(){
Objecto=newObject();
synchronized(o){
System.out.println("Hello World!");
        }
    }
publicstaticvoidmain(String[] args) {
for (inti=0; i<10; i++) {
newThread(()->{
Demo1.fun1();
            },String.valueOf(i)).start();
        }
    }
}

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

2、锁粗化

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

实例

publicclassHelloWorld {
publicstaticvoidmain(String[] args) throwsException {
Stringuser=newString("小明");
intcount=0;
//调用func1longstart1=System.nanoTime();
fun1(user,count);
System.out.println( System.nanoTime() -start1);
//调用func2longstart2=System.nanoTime();
fun2(user,count);
System.out.println( System.nanoTime() -start2);
    }
privatestaticvoidfun1(Stringuser,intcount){
for (inti=0; i<100; i++) {
count++;
        }
    }
privatestaticvoidfun2(Stringuser,intcount){
for (inti=0; i<100; i++) {
synchronized (user) {
count++;
            }
        }
    }
}

执行结果:

5034

5007

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

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

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

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

总结

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

下一篇主题

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

参考

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

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


致谢

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

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

相关文章
|
1月前
|
Java 开发工具
【Azure Storage Account】Java Code访问Storage Account File Share的上传和下载代码示例
本文介绍如何使用Java通过azure-storage-file-share SDK实现Azure文件共享的上传下载。包含依赖引入、客户端创建及完整示例代码,助你快速集成Azure File Share功能。
333 4
|
1月前
|
Java 数据处理 API
为什么你的Java代码应该多用Stream?从循环到声明式的思维转变
为什么你的Java代码应该多用Stream?从循环到声明式的思维转变
234 115
|
1月前
|
安全 Java 编译器
为什么你的Java代码需要泛型?类型安全的艺术
为什么你的Java代码需要泛型?类型安全的艺术
171 98
|
1月前
|
Java 编译器 API
java最新版和java8的区别,用代码展示
java最新版和java8的区别,用代码展示
230 43
|
1月前
|
安全 Java 容器
告别空指针噩梦:Optional让Java代码更优雅
告别空指针噩梦:Optional让Java代码更优雅
352 94
|
算法 安全 Java
Java 性能优化:35个小细节,让你提升Java代码运行的效率
  代码优化,一个很重要的课题。可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了。   代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨;但是如果有足够的时间开发、维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的。
349 0
|
机器学习/深度学习 算法 Java
11月27日云栖精选夜读 | Java性能优化的50个细节
在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身。养成良好的编码习惯非常重要,能够显著地提升程序性能。 1. 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: 第一,控制资源的使用,通过线程同步来控制资源的并发访问; 第二,控制实例的产生,以达到节约资源的目的; 第三,控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。
3096 0
|
Java 程序员 Android开发
10月31日云栖精选夜读 | Java性能优化的50个细节(珍藏版)
在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身。养成良好的编码习惯非常重要,能够显著地提升程序性能。 1. 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: 第一,控制资源的使用,通过线程同步来控制资源的并发访问; 第二,控制实例的产生,以达到节约资源的目的; 第三,控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。
3103 0
下一篇
oss云网关配置