「 代码性能优化 」作为一名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月前
|
算法 IDE Java
Java 项目实战之实际代码实现与测试调试全过程详解
本文详细讲解了Java项目的实战开发流程,涵盖项目创建、代码实现(如计算器与汉诺塔问题)、单元测试(使用JUnit)及调试技巧(如断点调试与异常排查),帮助开发者掌握从编码到测试调试的完整技能,提升Java开发实战能力。
241 0
|
2月前
|
Java 编译器 数据库连接
Java异常处理:写出更健壮的代码
Java异常处理:写出更健壮的代码
155 0
|
2月前
|
安全 Java 测试技术
Java 项目实战中现代技术栈下代码实现与测试调试的完整流程
本文介绍基于Java 17和Spring技术栈的现代化项目开发实践。项目采用Gradle构建工具,实现模块化DDD分层架构,结合Spring WebFlux开发响应式API,并应用Record、Sealed Class等新特性。测试策略涵盖JUnit单元测试和Testcontainers集成测试,通过JFR和OpenTelemetry实现性能监控。部署阶段采用Docker容器化和Kubernetes编排,同时展示异步处理和反应式编程的性能优化。整套方案体现了现代Java开发的最佳实践,包括代码实现、测试调试
125 0
|
2月前
|
存储 监控 算法
企业上网监控场景下布隆过滤器的 Java 算法构建及其性能优化研究
布隆过滤器是一种高效的数据结构,广泛应用于企业上网监控系统中,用于快速判断员工访问的网址是否为违规站点。相比传统哈希表,它具有更低的内存占用和更快的查询速度,支持实时拦截、动态更新和资源压缩,有效提升系统性能并降低成本。
60 0
|
2月前
|
SQL Java 数据库连接
Java 期末考试救急必备涵盖绝大多数核心考点及五大类经典代码助你过关
本文为Java期末考试复习指南,涵盖基础语法、面向对象编程、异常处理、文件操作、数据库连接五大核心考点,提供详细解析与实用代码示例,助力快速掌握重点,高效备考,轻松应对考试。
73 0
|
10月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
117 4
|
存储 安全 Java
Java面试题:请解释Java内存模型,并说明如何在多线程环境下使用synchronized关键字实现同步,阐述ConcurrentHashMap与HashMap的区别,以及它如何在并发环境中提高性能
Java面试题:请解释Java内存模型,并说明如何在多线程环境下使用synchronized关键字实现同步,阐述ConcurrentHashMap与HashMap的区别,以及它如何在并发环境中提高性能
124 0
|
安全 Java 开发者
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
203 0
|
安全 Java 编译器
Java多线程基础-6:线程安全问题及解决措施,synchronized关键字与volatile关键字(一)
线程安全问题是多线程编程中最典型的一类问题之一。如果多线程环境下代码运行的结果是符合我们预期的,即该结果正是在单线程环境中应该出现的结果,则说这个程序是线程安全的。 通俗来说,线程不安全指的就是某一代码在多线程环境下执行会出现bug,而在单线程环境下执行就不会。线程安全问题本质上是由于线程之间的调度顺序的不确定性,正是这样的不确定性,给我们的代码带来了很多“变数”。 本文将对Java多线程编程中,线程安全问题展开详细的讲解。
209 0