Java 并发编程:一文了解 synchronized 的使用

大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 027 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。

在当今的多核处理器时代,Java 并发编程变得尤为重要。为了充分利用计算资源,提高程序性能,编写高效、线程安全的并发代码成为每一个 Java 开发者的必修课。在 Java 的并发编程中,synchronized 关键字是最基础也是最常用的工具之一。

synchronized 关键字提供了一种简单且直接的方式来确保代码块或方法在多线程环境下的安全执行。通过对方法或代码块加锁,synchronized 可以防止多个线程同时访问共享资源,从而避免数据不一致的问题。然而,随着应用程序复杂性的增加和对高性能的需求,我们需要对 synchronized 有更深入的理解,以便在实际开发中灵活运用。

本篇文章将全面介绍 synchronized 的使用,从基本语法到锁的内部实现,再到锁的升级机制。无论你是并发编程的新手,还是有一定经验的开发者,这篇文章都将帮助你更好地理解和使用 synchronized,编写出更加高效和健壮的并发程序。

接下来,我们将从 synchronized 的基本概念和语法开始,逐步深入探讨其在 Java 并发编程中的重要角色。


文章目录

      • 1、synchronized 关键字简介
      • 2、synchronized 的修饰对象
        • 2.1、synchronized 修饰静态方法
        • 2.2、synchronized 修饰实例方法
        • 2.3、synchronized 修饰代码块
      • 3、对象的内存布局(64位)
        • 3.1、Mark Word
        • 3.2、Class Pointer
        • 3.3、Instance Data
        • 3.4、Padding Data
      • 4、Synchronized 锁升级过程
        • 4.1、偏向锁
        • 4.2、轻量级锁
        • 4.3、重量级锁


1、synchronized 关键字简介

在 Java 中,synchronized 关键字用于实现线程之间的同步,以确保多个线程在访问共享资源时不会出现竞态条件。synchronized 可以确保在任何给定时刻,最多只有一个线程可以执行被标记的代码块或方法,从而实现并发安全。

Synchronized 主要有以下三个作用:

  1. 原子性(Atomicity):通过互斥访问同步代码块或同步方法,保证同一时间只有一个线程能够执行这段代码,确保了操作的原子性。例如,两个线程同时执行一个同步方法时,只有一个线程能够获得锁并执行,另一个线程必须等待锁释放;

  2. 可见性(Visibility):保证线程对共享变量的修改对其他线程是可见的。具体来说,synchronized 会通过 Java 内存模型来实现可见性:当一个线程对变量进行 unlock 操作时,这些操作会同步到主内存中;而当线程对变量进行 lock 操作时,会清空工作内存中的变量值,从主内存中重新加载。这保证了其他线程在访问该变量时能够看到最新的值;

  3. 有序性(Ordering):通过 synchronized 解决指令重排序问题。Java 内存模型规定,一个 unlock 操作先行发生(happen-before)于后续对同一个锁的 lock 操作。这意味着,之前的操作(如变量的更新)在获取锁之前必须完成,从而避免了重排序导致的错误。

通过这三个机制,synchronized 能够有效地保证多线程环境下的并发安全。


2、synchronized 的修饰对象

synchronized 关键字可以用于修饰普通方法、静态方法和代码块,以实现线程同步,确保在同一时刻最多只有一个线程执行被锁定的代码段。

2.1、synchronized 修饰静态方法

synchronized 修饰静态方法时,锁定的是当前类的 Class 对象(类)。由于静态方法属于类,而不属于某个具体的对象实例,因此锁定的是整个类。

public class Example {public static synchronized void staticMethod() {// 静态方法体}
}
2.2、synchronized 修饰实例方法

synchronized 修饰实例方法时,锁定的是当前实例对象。每个对象实例都有自己的一把锁,因此不同实例的同步方法可以同时执行,但同一实例的同步方法不能同时执行。

public class Example {public synchronized void instanceMethod() {// 实例方法体}
}
2.3、synchronized 修饰代码块

synchronized 修饰代码块时,锁定的是 synchronized 括号里指定的对象。同步代码块可以精确地控制锁的作用范围,灵活性更高。同一时刻只有一个线程能够持有指定对象的锁,从而执行代码块内的代码。

public class Example {private final Object lock = new Object();public void method() {synchronized (lock) {// 同步代码块}}
}

需要注意的是,每个锁仅对当前代码块起作用,不会影响其他代码块的执行。因此,锁对象的选择非常重要,要根据具体需求选择合适的对象来进行同步。


3、对象的内存布局(64位)

在 Java 中,synchronized 关键字是基于对象锁来实现的。因此,理解 Java 对象在内存中的布局有助于更好地理解 synchronized 的底层实现。对于一个普通对象来说,它在内存中的布局分为四个部分:

image-20240804215344995

3.1、Mark Word

mark-word 是对象内存布局的核心部分,因为它存储了很多重要的信息。它占用 8 个字节,包含以下信息:

  • Hashcode:对象的哈希码(通常在对象第一次调用 hashCode() 方法时计算)。
  • 锁信息:用于表示对象的锁状态,如无锁状态、偏向锁、轻量级锁和重量级锁。
  • 分代年龄:用于表示对象在垃圾回收中的年龄。
  • GC 标志信息:用于垃圾回收标记。
3.2、Class Pointer

class pointer 存储的是该对象的类元数据的引用,通过它可以知道这个对象是哪个类的实例。这个指针也占用 8 个字节。

3.3、Instance Data

instance data 存储的是对象实例的实际数据,包括类中声明的所有实例变量的值。这个部分的大小取决于实例变量的数量和类型。

3.4、Padding Data

padding data 不一定会用到,其主要作用是保证整个对象所占的字节数是 8 的倍数,从而提高内存访问的效率。这样做是为了保证对象在内存中的对齐,以便于快速访问。

以下是一个简化的内存布局示意图:

+-------------------------+
|       Mark Word         | 8 bytes
+-------------------------+
|     Class Pointer       | 8 bytes
+-------------------------+
|     Instance Data       | n bytes
+-------------------------+
|     Padding Data        | 可选(0-7 bytes)
+-------------------------+

这种布局方式在 64 位 JVM 上尤为重要,因为内存对齐可以显著提升访问速度。了解这些信息有助于我们更深入地理解 synchronized 的工作机制,尤其是在涉及对象锁定和解锁时。、


4、Synchronized 锁升级过程

synchronized 锁有四种状态:无锁、偏向锁、轻量级锁、重量级锁。锁可以升级但不能降级,但偏向锁状态可以被重置为无锁状态。引入锁升级是为了降低获取锁的代价,因为在多数情况下不存在锁竞争,如果每次都要竞争锁会付出很多不必要的成本。以下是锁的升级过程:

4.1、偏向锁

偏向锁在线程第一次获取锁对象时,会在 Java 对象头和栈帧中记录偏向的锁的 ThreadID。当下次线程获取该锁时,会比较 ThreadID 是否一致:

  • 一致(线程1):直接进入,不需要使用 CAS(Compare And Swap)来加锁、解锁。
  • 不一致(线程2):检查对象的 ThreadID 线程是否还存活:
    • 存活:代表该对象被多个线程竞争,于是升级成轻量级锁。
    • 不存活:将锁重置为无锁状态,锁头重新标记线程为新的 ThreadID(抢占偏向锁失败的线程会触发锁膨胀至轻量级锁)。

如果线程 1 和线程 2 的执行时间刚好错开,那么锁只会在偏向锁之间切换,不会升级为轻量级锁,从而避开获取锁的成本,效率接近无锁状态。

4.2、轻量级锁

当对象被多个线程竞争(或关闭偏向锁功能)时,锁由偏向锁升级为轻量级锁。其他线程会通过 CAS + 自旋 的形式尝试获取锁。JDK 1.7 之后,引入了适应性自旋。简单来说,这次自旋获取到锁了,自旋的次数就会增加;这次自旋没拿到锁,自旋的次数就会减少。

  1. 如果后续线程是在持有锁的线程执行结束后抢锁,依然是轻量级锁,因为释放轻量级锁会恢复成无锁状态。
  2. 如果后续线程是在持有锁的线程执行结束前抢锁,就会触发膨胀成重量级锁。

轻量级锁获取过程:

在代码进入同步块时,如果同步对象锁状态为无锁状态,轻量级锁会构造一个 Lock Record 锁记录,用于存储锁对象目前的 Mark-Word 的拷贝。

public class Example {public void method() {synchronized (this) {// 代码块}}
}

拷贝成功后,虚拟机将使用 CAS 尝试将对象头的 Mark-Word 的 Lock-Word(锁记录指针) 指向当前线程 Lock Record 的起始地址,并将 Lock Record 的 owner 指向对象的 Mark-Word:

  • 如果更新成功,线程就拥有了该对象的锁,标志位设置为 00,表示此对象处于轻量级锁定状态。
  • 如果更新失败,虚拟机会检查对象的 Lock-Word 是否指向当前线程的 Lock Record。如果是,说明当前线程已经拥有了这个对象的锁,可以继续执行,否则说明多个线程竞争锁,锁升级为重量级。
4.3、重量级锁

当线程的自旋后依然未获取到锁,或者判定多个线程竞争锁时,为避免 CPU 无端耗费,锁由轻量级锁升级为重量级锁。

升级为重量级锁时,锁标志状态值变为 10,此时 Mark-Word 的 Lock-Word 指向重量级锁的指针,获取锁的同时会阻塞其他正在竞争该锁的线程,依赖对象内部的监视器(monitor)实现。monitor 又依赖操作系统底层,需要从用户态切换到内核态,成本非常高。

synchronized 中对 monitor 锁的实现用到了两个指令:monitorentermonitorexit(可通过 javap -verbose Example.class 反汇编查看)。

public class Example {public synchronized void method() {// 方法体}
}

synchronized 在 JVM 里的实现都是基于进入和退出 Monitor 对象来实现方法同步和代码块同步。可以把执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁。每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象计数器为 0。

  • monitorenter:执行 monitorenter 的线程尝试获得 monitor 的所有权,发生以下三种情况之一:
    1. 如果 monitor 的计数为 0,线程获得 monitor 并将计数设置为 1,线程成为 monitor 的所有者。
    2. 如果线程已经拥有了这个 monitor,则重新进入并累加计数。
    3. 如果其他线程已经拥有了这个 monitor,当前线程会被阻塞,直到计数变为 0,代表 monitor 已被释放,当前线程再次尝试获取 monitor。
  • monitorexit:monitorexit 将 monitor 的计数器减 1,直到减为 0,表示 monitor 已被释放,没有任何线程拥有它,其他等待的线程可以再次尝试获取 monitor。

在底层,monitor 依赖操作系统的 MutexLock(互斥锁)实现,因此重量级锁也称为互斥锁。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/391308.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

keil编译报错error:#8:missing closing quote 处理

MDK5采用UTF-8,提示很多个这样的error:#8:missing closing quote 的错误信息。提供以下几种方式解决: 在KEIL中Options for Target Flash -> C/C -> Misc Controls添加“--localeenglish”。

49 序列解包的多种形式和用法

序列解包(Sequence Unpacking)是 Python 中非常重要和常用的一个功能,可以使用非常简洁的形式完成复杂的功能,提高了代码的可读性,减少了程序员的代码输入量。 x, y, z 1, 2, 3 # 多个变量同时赋值 v_tuple (False…

【课程系列07】某乎AI大模型全栈工程师-第7期

网盘链接 链接:百度网盘 请输入提取码 --来自百度网盘超级会员v6的分享 课程目标 学习完毕咱们可以收获什么种能力: 1、传统前端 后端 数据分析 产品 绘图 算法工程等工作,一个人都可以实现,实现超级个体的能力 2、可以解决…

【C语言】Top K问题【建小堆】

前言 TopK问题:从n个数中,找出最大(或最小)的前k个数。 在我们生活中,经常会遇到TopK问题 比如外卖的必吃榜;成单的前K名;各种数据的最值筛选 问题分析 显然想开出40G的空间是不现实的&#…

基于STM32的温湿度监控系统

目录 引言环境准备工作 硬件准备软件安装与配置系统设计 系统架构硬件连接代码实现 初始化代码主循环代码应用场景 家居环境监控工业环境监控常见问题及解决方案 常见问题解决方案结论 1. 引言 在智能家居和工业自动化中,温湿度监控系统是一个非常重要的组成部分…

Java企业微信服务商代开发获取AccessToken示例

这里主要针对的是企业微信服务商代开发模式 文档地址 可以看到里面大致有三种token,一个是服务商的token,一个是企业授权token,还有一个是应用的token 这里面主要有下面几个参数 首先是服务商的 corpid 和 provider_secret ,这个可…

使用GCC编译Notepad++的插件

Notepad的本体1是支持使用MSVC和GCC编译的2,但是Notepad插件的官方文档3里却只给出了MSVC的编译指南4。 网上也没有找到相关的讨论,所以我尝试在 Windows 上使用 MinGW,基于 GCC-8.1.0 的 posix-sjlj 线程版本5,研究一下怎么编译…

快手商业化 Java后端 二面|面试官很nice

面试总结:没有那种纯八股问题,都是偏向于情景题。看到面试官最后出了一道多叉树的题目,我以为是想直接刷人,但还是尽力去尝试了一下,最后也没做出来,面试官很nice,在答不上来的时候会引导我去思…

JVM—垃圾收集算法和HotSpot算法实现细节

参考资料:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明 1、分代回收策略 分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取…

python实现小游戏——植物大战僵尸(魔改版本)

制作一款DIY的‘植物大战僵尸’游戏引起了很多人的兴趣。在这里,我将分享一个使用Python语言在PyCharm环境中开发的初始状态版本。这个版本主要应用了pygame库来完成,是一个充满创意和趣味的魔改版本。 文章目录 前言一、开发环境准备二、代码1.main方法…

Linux小组件:gcc

gcc 是C语言的编译器,在Linux下我们也用这个编译C语言 安装gcc sudo apt install build-essential 查看gcc版本信息 gcc --version 有时候会出现代码编译不过去的问题,通常可能是gcc的编译标准太低,不支持某些写法 比如在很多旧的编译标…

SQL注入实例(sqli-labs/less-4)

0、初始页面 1、确定闭合符号 前两条判断是否为数值型注入,后两条判断字符型注入的闭合符号 ?id1 and 11 ?id1 and 12 ?id1" ?id1") 2、确定表的列数 ?id1") order by 3 -- 3、确定回显位置 ?id-1") union select 1,2,3 -- 4、爆库…

【kali靶机之serial】--反序列化漏洞实操

kali靶机配置 【我图片里没有截图的默认配置即可】需要改的地方图片里面都有。 使用kali扫描网关的主机。 扫到一个开放了80端口HTTP协议的主机ip 访问80端口 会看到一个文本页面,翻译一下看是什么意思。。 F12查看cookie,是一个base64编码了的东西 使…

新手小白学习PCB设计,立创EDA专业版

本教程有b站某UP主的视频观后感 视频链接:http://【【教程】零基础入门PCB设计-国一学长带你学立创EDA专业版 全程保姆级教学 中文字幕(持续更新中)】https://www.bilibili.com/video/BV1At421h7Ui?vd_sourcefedb10d2d09f5750366f83c1e0d4a…

指标一致化处理

什么是数据指标 数据指标有别于传统意义上的统计指标,它是通过对数据进行分析得到的一个汇总结果,是将业务单元精分和量化后的度量值,使得业务目标可描述、可度量、可拆解。 数据指标有哪些类型 极大型:期望取值越大越好; 极小…

Memcached未授权访问漏洞

Memcached 是一套常用的 key-value 分布式高速缓存系统,由于 Memcached 的安全设计缺陷没有权限控制模块,所以对公网开放的Memcache服务很容易被攻击者扫描发现,攻击者无需认证通过命令交互可直接读取 Memcached中的敏感信息。 app"Mem…

AI电销机器人的效果与作用

ai电销机器人的工作效率是非常高的,电销机器人一天的外呼量至少是3000左右,工作效率是人工的十倍还多,并且电销机器人没有负面情绪,一直都可以保持高昂的工作热情,非常简单方便。 并且ai电销机器人是越用越聪明的&…

英国AI大学排名

计算机学科英国Top10 “计算机科学与信息系统”学科除了最受关注的“计算机科学”专业,还包括了“人工智能”“软件工程”“计算机金融”等众多分支专业。 1.帝国理工学院 Imperial College London 单以计算机专业本科来讲,仅Computing这个专业&#x…

来点八股文(五) 分布式和一致性

Raft raft 会进入脑裂状态吗?描述下场景,怎么解决? 不会。raft通过选举安全性解决了这个问题: 一个任期内,follower 只会投票一次票,且先来先得;Candidate 存储的日志至少要和 follower 一样新…

用uniapp 及socket.io做一个简单聊天app 4

界面如下&#xff1a; <template><view class"container"><input v-model"username" placeholder"用户名" /><input v-model"password" type"password" placeholder"密码" /><butto…