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

news2024/9/30 17:36:43

大家好,我是栗筝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.coloradmin.cn/o/1979919.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

练题模块环境搭建

文章目录 1.数据库表设计1.practice_set 套卷2.practice_set_detail 套卷细节3.practice_info 练习信息4.practice_detail 练习详情5.E-R图 2.架构设计(三层架构)3.练题微服务架构搭建1.创建一个练题微服务模块1.创建一个maven项目2.把src删除&#xff0…

类中特殊变量的初始化

在C的类中有一些变量的初始化需要进行特殊化的处理,这里我将列举出常见的两种特殊类型的变量初始化。 目录 const 类型数据的初始化 代码实例: static类型数据的初始化 代码实例: const 类型数据的初始化 对于const修饰的数据我们需要在…

Robot Operating System——单线程中启动多个Node

在《Robot Operating System——Service的同步/异步通信》一文中,我们介绍了如何实现Service的客户端和服务端。在例子中,它们分别被编译成libadd_two_ints_client_async_library.so和libadd_two_ints_server_library.so,然后分别被可执行程序…

C:将代码拆分放在多个文件的操作

目录 前言: 1、多个文件 2、将一个程序分为多个文件的好处 3、一定程度上对代码进行隐藏 结语: 前言: 在我们刚开始学习C语言时,编写的代码通常比较简短,因此将其放在一个文件中并不会带来不便。然而,…

17965 幸运之星(优先做)

这个问题可以通过使用递归或者迭代的方法来解决。我们可以使用一个一维数组dp来存储中间结果&#xff0c;dp[i]表示i个人时的“幸运之星”的初始编号。 以下是使用C的代码实现&#xff1a; #include <iostream> using namespace std;const int MAXN 1000000; int dp[M…

力扣:100379. 新增道路查询后的最短距离 I(Java,BFS)

目录 题目描述&#xff1a;示例 &#xff1a;代码实现&#xff1a; 题目描述&#xff1a; 给你一个整数 n 和一个二维整数数组 queries。 有 n 个城市&#xff0c;编号从 0 到 n - 1。初始时&#xff0c;每个城市 i 都有一条单向道路通往城市 i 1&#xff08; 0 < i < …

web高可用群集架构部署----超详细

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

【笔记1-6】Qt bug记录:Qt6 无法使用qsort函数排序

在进行Qt5向Qt6升级的过程中&#xff0c;发现Qt6会编译时会出现以下错误&#xff0c;找不到qsort的定义 一开始以为应该是需要头文件或者.pro文件追加一些配置的问题&#xff0c;但是按照下面的提示追加了两个头文件后也没有效果 再进一步调查&#xff0c;找到了下面的文章&a…

python3 pyside6图形库学习笔记及实践(三)

目录 前言菜单栏相关控件使用QtDesigner快速构建菜单栏结构语法 上下文菜单概念为窗体添加上下文菜单为控件添加上下文菜单 折叠菜单资源的加载内置图标Rcc的使用创建资源文件加载资源文件 前言 本系列文章为b站PySide6教程以及官方文档的学习笔记 原视频传送门&#xff1a;【…

【Linux 17】进程信号

文章目录 &#x1f308; 一、信号的概念⭐ 1. 什么是信号⭐ 2. 常见的信号⭐ 3. 信号的管理 &#x1f308; 二、进程的运行⭐ 1. 进程运行模式⭐ 2. 查看后台进程⭐ 3. 运行后台进程⭐ 4. 终止后台进程 &#x1f308; 三、信号的产生⭐ 1. 通过键盘产生信号⭐ 2. 调用系统函数向…

YiYi-Web项目技术栈介绍

项目地址&#xff1a;https://gitee.com/jack0240/yiyi-web YiYi后台管理系统&#xff08;不分离版&#xff09;&#xff0c;SpringBoot Thymeleaf Layui 后台管理系统框架。 前端技术栈 HTML JavaScript JQuery Layui、Bootstrap Echarts图表、大屏展示、富文本 进阶&#…

书生大模型实战营第三期——入门岛

第一关&#xff1a;Linux基础知识 任务如下&#xff1a; 任务描述闯关任务完成SSH连接与端口映射并运行hello_world.py可选任务 1将Linux基础命令在开发机上完成一遍可选任务 2使用 VSCODE 远程连接开发机并创建一个conda环境可选任务 3创建并运行test.sh文件 1. 使用密码进行…

数据结构——单向链表

目录 前言 一、单向链表 二、单向链表基本操作 1、链表单创建 2.节点插入 &#xff08;1&#xff09;尾部插入 &#xff08;2&#xff09;任意位置插入 3、单向链表节点删除 4、链表打印 5、释放链表 6、链表逆序 ...... 三、链表测试 总结 前言 链表&#xff08;Linked List&a…

单细胞Seurat的umi矩阵-与feature、counts(用于质控)

目录 关于umi矩阵学习 用umi计算feature、counts值 ①meta数据查看 ②Count和Feature计算(生成Seurat时自动计算) 1)提取UMI矩阵 2)计算 其他指标 评估质量指标(重点) 1)UMI计数 2)基因计数 3)UMIs vs. genes detected 4)线粒体计数比率 5)综合过滤 过…

【C语言篇】文件操作(下篇)

文章目录 前言文件的顺序读写fscanf和fprintffread和fwrite 文件的随机读写fseekftellrewind 文件读取结束的判定容易被错误使用的feof 文件缓冲区 前言 本篇接上一篇文件操作&#xff08;上篇&#xff09;的内容 文件的顺序读写 在上一篇已经介绍了前面四个了&#xff0c;接…

【人工智能基础四】循环神经网络(RNN)与长短时记忆网络(LSTM)

文章目录 一. RNN1. 循环神经网络结构2. 循环神经网络计算2.1. 机器翻译2.2. 循环体 二. 长短时记忆网路&#xff08;LSTM&#xff09;1. 产生背景2. LSTM的设计思想与LSTM的链式结构2.1. LSTM的设计思想2.2. LSTM链式结构图与遗忘门 3. 长短时记忆网络结构 一. RNN RNN出现的…

五种多目标算法(MOGOA、MOMA、MODA、MOPSO、NSGA2)性能对比(MATLAB代码)

一、算法介绍 MOGOA&#xff1a;多目标蝗虫优化算法 MOMA&#xff1a;多目标蜉蝣算法 MODA&#xff1a;多目标蜻蜓算法 MOPSO&#xff1a;多目标粒子群优化算法 NSGA2&#xff1a;非支配排序遗传算法II 这些算法都是针对多目标优化问题设计的元启发式算法&#xff0c;每种…

Java | Leetcode Java题解之第321题拼接最大数

题目&#xff1a; 题解&#xff1a; class Solution {public int[] maxNumber(int[] nums1, int[] nums2, int k) {int m nums1.length, n nums2.length;int[] maxSubsequence new int[k];int start Math.max(0, k - n), end Math.min(k, m);for (int i start; i < e…

C语言 | Leetcode C语言题解之第321题拼接最大数

题目&#xff1a; 题解&#xff1a; int compare(int* subseq1, int subseq1Size, int index1, int* subseq2, int subseq2Size, int index2) {while (index1 < subseq1Size && index2 < subseq2Size) {int difference subseq1[index1] - subseq2[index2];if (…

Codeforces Round 963 (Div. 2) A~C

封面原图 画师やんよ 掉大分的一场 连夜补题 真的不会写啊真的红温了 A - Question Marks 题意 选择题中答案为ABCD的题目各有n道&#xff0c;小明的答案给你&#xff0c;其中&#xff1f;表示这道题空着没写&#xff0c;问他的最高得分 思路 空着的题目肯定没分 超出选项…