news 2026/10/9 3:29:43

synchronized详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
synchronized详解

文章目录

  • 1、并发编程会出现原子性、可见性、有序性问题。
  • 2、JVM内存模型
  • 3、主内存与工作内存的交互
  • 4、synchronized如何保证可见性、原子性、有序性?
  • 5、synchronized的特性
    • 5.1 可重入
    • 5.2 不可中断
  • 6、synchronized的原理(jdk1.6以前)
    • 6.1 synchronized代码块:
      • JVM规范对monitorenter的描述:
      • JVM规范对monitorexit的描述:
    • 6.2 synchronized方法:
    • 6.3 总结:
  • 7、synchronized的优化(jdk1.6及以后)
  • 8、平时代码中对synchronized进行优化的建议:

1、并发编程会出现原子性、可见性、有序性问题。

原子性:在一次或多次操作中,要么所有的操作都执行并且不会受其它因素干扰而中断,要么所有的操作都不执行。在多线程环境下,线程对共享变量的操作,要么成功,要么失败,不会受其它线程的干扰。

可见性:在多线程环境下,某个线程对共享变量的修改,其它的线程可以知道并获取最新修改的值。

有序性:指程序中代码的执行顺序,java在编译时和运行时对代码进行优化,会导致程序的最终执行顺序不一定是我们Java代码编写时的顺序。

2、JVM内存模型

不同 CPU 硬件,原生的指令重排、数据可见行为不一样。统一的多线程内存语义,就是 Java 定义一套固定的并发行为规则。开发者使用 volatile、synchronized 时,无论跑在 x86 还是 ARM,多线程的可见性、有序性表现完全相同。底层硬件差异交给 JVM,由 JVM 根据当前 CPU,生成对应的内存屏障等硬件指令,开发者不需要感知底层硬件区别。

Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。

主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。

每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。

Java内存模型的作用:Java内存模型是一套在多线程读写共享数据时,对共享数据的可见性、有序性、和原子性的规则和保障。主要就是synchronized和volatile。

3、主内存与工作内存的交互

Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。

只有使用synchronized才会有Lock和unlock操作。

  1. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值
  2. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中

4、synchronized如何保证可见性、原子性、有序性?

保证可见性:

由于使用synchronized加锁时,会清空工作内存中的变量的值,导致线程重新读取主存中的最新的值。

从而保证了线程的可见性。使用volatile也可以保证可见性。

保证原子性:

使用synchronized加锁,线程需要获取对象的锁,由于对象的锁,只有一把,同一时间只有一个线程能操作加锁的代码,从而保证了原子性。使用volatile无法保证原子性。

保证有序性:

加了synchronized依然会发生指令重排序,但是我们的synchronized会保证同步块在同一时间只有一个线程访问,就是单线程访问同步代码块,那么在单线程情况下,管你指令怎么重排,指令执行的结果都是一样的,如果指令重排导致在单线程下执行结果不一致,那么也就不会发生指令重排了。从而保证了有序性。使用volatile也可以保证有序性。


为什么要发生指令重排?

为了提高程序的执行效率,编译器和CPU会对程序中的代码进行重排序。

什么情况下可以发生指令重排序?

我们要满足as-if-serial语义:即不管编译器和CPU如何进行重排序,必须保证在单线程环境下,程序的结果是正确的。

5、synchronized的特性

5.1 可重入

一个线程可以重复获取某个对象的锁,即重复获取同一把锁。

可重入原理:

synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,获取锁计数器+1,释放锁计数器-1。

可重入的好处:

  1. 可以避免死锁
  2. 可以使我们更好的封装代码

5.2 不可中断

不可中断是指一个线程在获取锁之后,另一个线程想要获取锁,获取不到处于阻塞或等待状态,如果第一个线程不释放锁,那么阻塞或等待的线程就一直等,并且不可被中断。

synchronized:是不可中断的

Lock:

6、synchronized的原理(jdk1.6以前)

synchronized是JVM内置锁,基于Monitor机制实现。依赖底层操作系统的互斥原语Mutex(互斥量)。
Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。

Java(HotSpot)中的Monitor是基于C++实现的,由ObjectMonitor实现的。

// 初始化monitor,除了semaphore,其他字段都是简单的int或者指针类型ObjectMonitor(){_header=NULL;_count=0;_waiters=0,_recursions=0;_object=NULL;_owner=NULL;_WaitSet=NULL;_WaitSetLock=0;_Responsible=NULL;_succ=NULL;_cxq=NULL;FreeNext=NULL;_EntryList=NULL;_SpinFreq=0;_SpinClock=0;OwnerIsThread=0;_previous_owner_tid=0;}

ObjectMonitor 的主要参数都在里面,从名字上慢慢分析。

_WaitSet和 _EntryList有什么区别呢?

当多个线程同时访问同步代码时,首先进入的就是_EntryList 。当获得对象的monitor时,_owner 指向当前线程,_count进行加1。
​若持有monitor的线程调用wait() ,则释放持有的monitor ,_owner 变为null ,_count 减1。
​同时该线程进入_WaitSet 等待被唤醒。如果执行完毕,也释放monitor。

6.1 synchronized代码块:

将synchronized代码块使用javap对字节码进行反编译时,会发现在代码块的前后插入一个monitorenetr和monitorexit的指令。

JVM规范对monitorenter的描述:

每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获

取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应

的monitor的所有权。其过程如下:

  1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为
    monitor的owner(所有者)
  2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直
    到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

小结:

synchronized的锁对象会关联一个monitor,这个monitor对象不是我们创建的,是JVM的线程在执行到这个同步代码块时,如果发现没有monitor与锁对象关联,则会创建该对象(C++的对象),这个对象有两个成员变量:owner:记录拥有该monitor的线程,recursions:记录线程拥有锁的次数,当一个线程拥有monitor后,其他线程只能等待。

JVM规范对monitorexit的描述:

  1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
  2. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出
    monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个
    monitor的所有权。

小结:
monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。

6.2 synchronized方法:

同步方法在通过反汇编可以看到会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenetr和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit。本质还是monitor。

6.3 总结:

通过反汇编可以看到在synchronized同步代码是由monitorenter指令开始,monitorexit指令结束,每个锁对象会关联一个ObjectMonitor对象(它才是真正的锁对象),这个ObjectMonitor对象是由JVM创建的,里面有两个成员变量:owner:拥有该锁对象的线程,recursions:该线程获取锁的次数,当JVM执行到某个线程的某个方法内的monitorenter指令,jvm会判断当前锁对象是否有ObjectMonitor关联,没有就创建,并将owner设置为当前线程,recursions+1,该线程每重入一次,recursions就加一,当执行到monitorexit指令时,recursions-1,直到减为0,说明执行到monitorexit了,这个线程就会释放锁,其他被monitor对象阻塞的线程就可以竞争该锁。

ObjectMonitor对象中有enter和exit方法,当T1线程准备获取锁时,发现ObjectMonitor:owner拥有锁的线程不是自己时,就会调用enter把自己加入到用户态的一个等待队列中,并调用park(JVM)—> mutex_lock(库api)—> 调用内核的FUTEX_WAIT原语,将自己挂起。当其他线程释放锁时,根据用户态的等待队列获取队头等待线程,然后调用unpark(JVM)—>mutex_lock(库api)—> 调用内核FUTEX_WAKE原语,将其唤醒,进行争抢锁。

Linux:futex(FUTEX_WAIT/FUTEX_WAKE) 不通系统的阻塞和唤醒原语不同


7、synchronized的优化(jdk1.6及以后)

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的futex(FUTEX_WAIT/FUTEX_WAKE)原语实现,线程挂起和唤醒需要涉及系统调用,需要用户态到内核态的切换,切换成本非常高。

主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。所以说synchronized是一个重量级锁,重量级的操作。

针对synchorized的重量级操作及RetrantLock轻量级锁的出现,JDK在1.6版本对synchronized进行了优化,主要有三方面:

7.1 锁升级

无锁 —> 偏向锁 —> 轻量级锁 —> 自旋锁 —> 重量级锁

jdk1.6对synchronized进行优化,减低了直接升级为重量级锁而耗费大量的资源,降低线程获取锁的代价,synchronized的优化会涉及对象头。它是将锁保存在对象头中。

对象头组成:

64位虚拟机Mark World的存储结构:

对象头= Mark Word + 类型指针(未开启指针压缩的情况下)

实例数据:类中定义的成员变量

字节填充:不一定有,Hotspot虚拟机对象的大小是8字节整数倍,不是8字节整数倍会进行字节填充。


偏向锁延迟及匿名偏向锁:

虚拟机默认在4秒后开启偏向锁功能:这叫做偏向锁延迟

  1. 在4s内创建对象,此时对象的对象头的锁标志位:01,偏向标志位:0
  2. 在4s后创建对象,此时对象的对象头的锁标志位:01,偏向标志位:1 (匿名偏向锁,没有存储线程id)

偏向锁在升级为轻量级锁时,会涉及到偏向锁撤销,需要等到一个安全点(STW),才可以做偏向锁撤销,在明知道有并发情况,就可以选择不开启偏向锁,或者是设置偏向锁延迟开启。

因为JVM在启动时,需要加载大量的.class文件到内存中,这个操作会涉及到synchronized的使用,为了避免出现偏向锁撤销操作,JVM启动初期,有一个延迟4s开启偏向锁的操作

偏向锁的加锁和撤销流程:

轻量级锁的加锁流程:

  1. 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝
  2. 使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针
    1. 更新成功:
      • 当前线程加轻量级锁成功,锁对象的锁标志位变为“00”
    2. 更新失败:
      • 说明至少存在一条线程与当前线程竞争获取该对象的锁
        1. 检查对象的Mark Word是否指向当前线程的栈帧
          1. 是:说明当前线程拥有这个对象的锁(可重入),直接进入同步块执行
          2. 否:说明这个锁对象已经被其他线程抢占,抢锁线程自旋,自旋一定次数后轻量级锁膨胀为重量级锁

轻量级锁解锁流程:

  1. 如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来
    1. 替换成功:说明同步过程就顺利完成,锁对象转为无锁状态
    2. 替换失败:说明有其他线程尝试过获取该锁,并且将锁升级为重量级锁,在释放锁的同时,唤醒被挂起的线程

总结:

当对象处于锁状态时,收到计算hashCode的请求:

锁升级:无锁 —> 偏向锁 —> 轻量级锁(自旋锁) —> 重量级锁,锁升级是单向的,不能回退,只能偏向锁能退到无锁再进行升级,轻量级锁和重量级锁即使没线程执行,隔断时间来了,还是轻量级和重量级锁起步,除非锁对象被GC回收,不然永远不会回退。

锁撤销:

jdk15关闭了偏向锁,jdk18彻底移除偏向锁,主要原因:

  1. 现代 CPU 的 CAS 原子指令开销大幅下降
    偏向锁诞生年代(JDK1.6),CAS 代价很高,所以做偏向锁优化,尽量避免 CAS。现在 CPU 硬件进步,CAS 已经很快,偏向锁带来的性能收益很小。
  2. 偏向锁撤销(revoke)代价极高,需要 STW 全局暂停
    一旦发生锁竞争,触发偏向撤销就要 STW 暂停所有线程。
    在线程池场景(大量线程交替复用锁),频繁触发撤销,反而性能变差,很多业务关掉偏向锁性能更高。
  3. JVM 同步子系统代码极其复杂,维护成本高
    偏向锁增加了大量复杂的分支逻辑(重偏向、批量重偏向、批量撤销),给后续 JVM 锁优化、并发改造带来巨大阻碍,为了简化 HotSpot 同步代码,决定删掉这个优化。

JIT即时编译做了哪些优化?

7.2 锁消除:

锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

publicclassDemo01{publicstaticvoidmain(String[]args){contactString("aa","bb","cc");}publicstaticStringcontactString(Strings1,Strings2,Strings3){returnnewStringBuffer().append(s1).append(s2).append(s3).toString();}}

7.3 锁粗化:

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

publicclassDemo01{publicstaticvoidmain(String[]args){StringBuffersb=newStringBuffer();for(inti=0;i<100;i++){sb.append("aa");}System.out.println(sb.toString());}}

8、平时代码中对synchronized进行优化的建议:


引用:
https://blog.csdn.net/v123411739/article/details/117401299?spm=1001.2014.3001.5501
https://blog.csdn.net/qq_40788718/article/details/106450724
https://www.bilibili.com/read/cv14772871/
https://juejin.cn/post/6844903640197513230
https://xiaohuang.blog.csdn.net/article/details/129848342

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 3:29:09

CSS图片实例

本文目录1. 前言2. 普通图片3. 圆角图片4. 缩略图效果5.小结1. 前言 上一篇我们详细讲解了如何利用CSS&#xff0c;来制作一个好看的按钮。 本篇我们来研究下如何用CSS美化图片。 2. 普通图片 普通情况下&#xff0c;我们给图片设置个宽度和高度即可。 普通图片&#xff1a…

作者头像 李华
网站建设 2026/10/9 3:29:00

Django实战:42文件源码拆解课程推荐与智能问答系统

简介&#xff1a;这份资源是一套基于Python的实时课程教学数据内容推荐与个性化智能问答系统源码&#xff0c;面向教育技术方向的学习者、课程设计开发者及毕业设计选题人群&#xff0c;用于解决教学资源个性化推送与知识问答自动化的实现问题。压缩包共54个文件&#xff0c;约…

作者头像 李华
网站建设 2026/10/9 3:28:45

民航iOS技术栈与离线优先架构:从Swift到航班动态的工程实践

1. 民航场景让我重新理解了 iOS 技术栈很多人以为民航 App 就是另一个“航班查询工具”&#xff0c;真正做过之后才发现完全是另一回事。我在民航 IT 一线做了几年 iOS 开发&#xff0c;从旅客端到机组端都参与过&#xff0c;对这套业务有很深的体感。标题里说的“技术栈、架构…

作者头像 李华
网站建设 2026/10/9 3:28:45

子网DHCP实战:单台服务器为多VLAN分配IP的配置与地址池规划

1. 子网DHCP&#xff1a;为什么你的实验要玩“一拖多”很多朋友在ENSP里做DHCP实验&#xff0c;习惯性操作就是“一台路由器 两台PC”&#xff0c;然后接口模式下敲几条命令&#xff0c;PC自动获取到IP&#xff0c;实验结束。这套流程练熟之后&#xff0c;你会觉得DHCP不过如此…

作者头像 李华
网站建设 2026/10/9 3:28:41

Git Reset全面详解:三种模式与reflog救援实战

2. 前言&#xff1a;为什么 Git Reset 是开发者最该吃透的命令之一在Git的日常操作里&#xff0c;git reset绝对算得上使用频率最高、误操作后果最严重的命令之一。很多新手对它敬而远之&#xff0c;老手也常在紧急时刻因为记混参数而手忙脚乱。我见过不少团队因为一次随意的gi…

作者头像 李华