【JavaEE】synchronized 原理

news2024/11/18 13:35:57

在这里插入图片描述

文章目录

  • 前言
  • synchronized 的加锁过程
    • 1.无锁 -> 偏向锁
    • 2. 偏向锁 -> 轻量级锁
    • 3. 轻量级锁 -> 重量级锁
  • 锁的优化操作
    • 1. 锁消除
    • 2. 锁粗化
  • 相关面试题

前言

前面我们学习了关于JavaEE多线程方面的锁策略以及 synchronized 分别对应哪些锁策略,并且我们还了解了关于 CAS 操作在某些情境下不需要加锁而避免因竞争锁造成的阻塞等待状态。今天我将为大家分享 synchronized 的加锁过程以及编译器对加锁过程的一些优化操作。

synchronized 的加锁过程

当我们使用 synchronized 进行枷锁的时候,往往不是立即就对该线程进行加锁的,而是需要经过一个 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 的过程,那么接下来我们来看看这个过程是如何进行的。

在这里插入图片描述

synchronized 在 Java 6 之后进行了优化,引入了无锁、偏向锁、轻量级锁和重量级锁四种状态,这四种状态会随着竞争情况逐渐升级。锁可以升级但不能降级。

1.无锁 -> 偏向锁

当我们使用 synchronized 进行加锁的时候,并不会立即从 无锁的状态转换为加锁 的状态的,而是会先处于一个偏向锁的状态,什么叫做偏向锁呢?

偏向锁并不是真正的“加锁”,而是给对象头中做一个“偏向锁的标记”,记录这个锁属于哪个线程。如果后续没有其他线程来竞争该锁,那么就不用进行其他同步操作了(避免了加锁解锁的开销);如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了,很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态。

偏向锁就有点类似于前面我们学习单例模式时候的懒汉模式,就是能不加锁的时候就尽量不加锁,避免不必要的开销。

2. 偏向锁 -> 轻量级锁

当有其他线程开始跟当前线程竞争锁的时候,因为该线程已经在对象头中进行了标记,那么该线程就可以直接获取到这个锁,进入轻量级锁的状态,这个轻量级锁也就是自适应的自旋锁,而这个自旋锁就是由 CAS操作实现的,防止进入内核态操作使线程进入阻塞等待状态。

  • 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
  • 如果更新成功, 则认为加锁成功
  • 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU)

3. 轻量级锁 -> 重量级锁

如果后面发生锁竞争比较激烈的话,synchronized 就会从 轻量级锁转换为重量级锁(挂起等待锁)会使线程进入阻塞等待状态。

  • 执行加锁操作, 先进入内核态.
  • 在内核态判定当前锁是否已经被占用
  • 如果该锁没有占用, 则加锁成功, 并切换回用户态.
  • 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
  • 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒
    这个线程, 尝试重新获取锁

锁的优化操作

  1. 锁消除
  2. 锁粗化

1. 锁消除

Java 锁消除(Lock Elimination)是 Java 虚拟机(JVM)中的一种优化技术,用于消除不必要的同步锁操作,从而提高程序的性能和并发性。

在 Java 中,synchronized 关键字可以用于实现同步和互斥,以确保多个线程对共享资源的访问的正确性。然而,synchronized 也会引入一定的开销,包括获取锁、执行同步代码块、释放锁等操作的时间成本,以及可能导致线程阻塞和上下文切换的代价。

为了减少 synchronized 带来的开销,JVM 使用了锁消除优化技术。锁消除的基本原理是:如果 JVM 检测到某个同步代码块中不存在共享数据的竞争访问,即该代码块不会被多个线程同时访问,那么 JVM 可以安全地消除该代码块的同步锁操作。

public class Demo2 {
    private static int num;
    public static void main(String[] args) {
        synchronized (Demo2.class) {
            for(int i = 0; i < 10; i++) {
                num += i;
            }
            System.out.println(num);
        }
    }
}

在这个例子中,synchronized 代码块中只有一个成员变量 num,并且没有其他线程可以访问到它。因此,JVM 可以安全地消除该代码块的同步锁操作,从而提高程序的性能。

需要注意的是,锁消除只是一种优化技术,不能保证在所有情况下都能消除同步锁操作。此外,如果程序中存在真正的并发竞争访问,那么使用 synchronized 仍然是必要的,以确保数据的正确性和一致性。

锁消除是编译器在编译阶段就进行的操作,而偏向锁则是在代码的运行过程中实现的。

2. 锁粗化

因为进行加锁和解锁都会造成资源的浪费,所以在一些情况下就可以减少加锁和解锁的次数,也就是锁粗化的操作。

锁粗化的基本思路是将多个相邻的同步代码块合并为一个更大的同步代码块,以减少获取和释放锁的次数。这样可以降低同步操作的开销,并减少线程阻塞和上下文切换的可能性。

public class Demo3 {
    private static int num;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 100; i++) {
                synchronized (Demo2.class) {
                    num++;
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 100; i++) {
                synchronized (Demo2.class) {
                    num++;
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(num);
    }
}

上面这个代码,两个线程中的任务都是循环了100次,但是每次循环都需要进行加锁和解锁的操作,这样会浪费大量的资源,所以这个代码就可以进行锁粗化的优化操作。

public class Demo3 {
    private static int num;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (Demo2.class) {
                for(int i = 0; i < 100; i++) {
                    num++;
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (Demo2.class) {
                for(int i = 0; i < 100; i++) {
                    num++;
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(num);
    }
}

当进入线程的时候就进行加锁,当循环100次结束之后再进行解锁操作,这样就避免了资源的浪费。

并不是所有的加锁操作都可以进行锁粗化的优化,我们要保证最终代码的执行结果是正确的情况下才能做出相应的优化操作。

相关面试题

1) 什么是偏向锁?

偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程). 如果没有其他线
程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销. 一旦真的涉及到其他的线程竞
争, 再取消偏向锁状态, 进入轻量级锁状态.

2) synchronized 实现原理 是什么?

本博客所写的内容

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

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

相关文章

读富爸爸财务自由之路后感

&#xff08;点击即可收听&#xff09; 再比如&#xff0c;S象限的人把自己的经验复制、业务扩大&#xff0c;开办公司&#xff0c;从而拥有B象限的收入 当你在左右两侧象限都有收入时&#xff0c;就像两条腿走路的人&#xff0c;才是真正安全。 但财务安全还不等于财务自由&am…

高精度算法模板

1.加法 string a1, b1; int a[5010], b[5010], c[5010]; signed main() {cin >> a1 >> b1;int len1 a1.size();int len2 b1.size();for (int i 1; i < len1; i) {a[i] a1[len1 - i] - 0;}for (int i 1; i < len2; i) {b[i] b1[len2 - i] - 0;}for (in…

想要精通算法和SQL的成长之路 - 岛屿数量和岛屿的最大面积

想要精通算法和SQL的成长之路 - 岛屿数量和岛屿的最大面积 前言一. 岛屿数量1.1 并查集数据结构构造1.2 使用并查集编码 二. 岛屿的最大面积 前言 想要精通算法和SQL的成长之路 - 系列导航 并查集的运用 一. 岛屿数量 原题链接 从这个题目的特性来看&#xff0c;它适合用并查集…

Vue中如何进行日历展示与操作

在Vue中创建交互式日历应用 在Web开发中&#xff0c;创建一个交互式的日历应用是一项常见的任务。Vue.js作为一个流行的JavaScript框架&#xff0c;提供了许多便捷的工具和组件来简化日历的开发。本文将介绍如何使用Vue来创建一个简单但功能强大的日历应用&#xff0c;包括展示…

linux入门---信号的理解

目录标题 如何理解计算机中的信号如何查看计算机中的信号初步了解信号的保存和发送如何向目标进程发送信号情景一&#xff1a;使用键盘发送信号情景二&#xff1a;系统调用发送信号情景三&#xff1a;硬件异常产生信号情景四&#xff1a;软件条件产生信号 核心转储信号的两个问…

CentOS如何查找java安装路径

目 录 背景 详细步骤 1.使用指令查看有关javad安装路径 2.填入java路径 3.查找java安装路径 4.配置文件展示 背景 准备部署分布式hadoop的时候&#xff0c;校验hadoop版本发现java没配置 但是又有java版本信息 详细步骤 1.使用指令查看有关javad安装路径 java -verb…

敏捷项目管理中产品负责人– PO的核心职责

在敏捷项目管理中&#xff0c;产品负责人的角色非常重要。他们代表利益相关者&#xff0c;负责确保团队开发的产品具有价值、符合期望&#xff0c;满足客户需求。 产品负责人核心职责有&#xff1a; 规划产品的方向和路线图&#xff0c;决定产品要做什么。清晰的将产品的路线…

基于Java的银行记账与审核系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

使用win_b64做CATIA的开发测试

文章目录 一、把win_b64文件夹放在无中文的路径下二、启动环境文件编辑器三、新建环境文件四、集成CAA代码五、去用户环境目录查看环境文件是否生成六、关闭环境文件编辑器七、去桌面双击新建的快捷方式即可启动CATIA&#xff0c;进行测试八、进入对应的开发模块&#xff0c;查…

postgresql-聚合函数增强功能

postgresql-聚合函数增强功能 按季度统计入职员工 按季度统计入职员工 select -- extract截取&#xff0c;按季度进行统计入职员工总数 extract(year from hire_date), count(*) filter(where extract(quarter from hire_date) 1) "第一季度", count(*) filter(wh…

计算机竞赛 题目: 基于深度学习的疲劳驾驶检测 深度学习

文章目录 0 前言1 课题背景2 实现目标3 当前市面上疲劳驾驶检测的方法4 相关数据集5 基于头部姿态的驾驶疲劳检测5.1 如何确定疲劳状态5.2 算法步骤5.3 打瞌睡判断 6 基于CNN与SVM的疲劳检测方法6.1 网络结构6.2 疲劳图像分类训练6.3 训练结果 7 最后 0 前言 &#x1f525; 优…

【软考】5.1 七层模型/局域网/TCP-IP协议

《网络功能和分类》 即计算机技术与通信技术相结合的产物&#xff0c;实现了远程通信、远程信息处理和资源共享计算机网络的功能&#xff1a;数据通信、资源共享、负载均衡&#xff08;给多个服务器负担&#xff09;、高可靠性 分布范围 拓扑结构分类 总线型&#xff1a;一般局…

Git小书系列笔记

Git准备 首先根据自己的系统安装git&#xff0c;安装成功后可以通过如下指令查看git版本。 使用Git之前&#xff0c;需要配置用户名称和电子邮件。 1.设置全局的用户名和电子邮件 git config --global user.name "Your Name" git config --global user.email &quo…

OpenGLES:绘制一个混色旋转的3D立方体

效果展示 混色旋转的3D立方体 一.概述 之前关于OpenGLES实战开发的博文&#xff0c;不论是实现相机滤镜还是绘制图形&#xff0c;都是在2D纬度 这篇博文开始&#xff0c;将会使用OpenGLES进入3D世界 本篇博文会实现一个颜色渐变、旋转的3D立方体 动态3D图形的绘制&#xf…

RabbitMQ-java使用消息队列

1 java操作消息队列 1.1 java实现生产者 新建一个springboot项目&#xff0c;导入依赖 <dependency><groupId>com.rabbitmq</groupId><artifactId>amqp-client</artifactId><version>5.14.2</version> </dependency>导入依…

ctfshow web入门 php特性 web113-web125

1.web113 和上题一样,/proc/self/root代表根目录&#xff0c;进行目录溢出&#xff0c;超过is_file能处理的最大长度就不认为是个文件了 payload: compress.zlib://flag.php /proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/p roc/self/root/p…

操作系统内存管理相关

1. 虚拟内存 1.1 什么是虚拟内存 虚拟内存是计算机系统内存管理的一种技术&#xff0c;我们可以手动设置自己电脑的虚拟内存。不要单纯认为虚拟内存只是“使用硬盘空间来扩展内存“的技术。虚拟内存的重要意义是它定义了一个连续的虚拟地址空间&#xff0c;并且 把内存扩展到硬…

SLAM从入门到精通(用python实现机器人运动控制)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 在ROS下面&#xff0c;开发的方法很多&#xff0c;可以是c&#xff0c;可以是python。大部分接口操作类的应用&#xff0c;其实都可以用python来开…

(五)激光线扫描-位移台标定

线激光属于主动测量方式,但是由于线激光的特性,我们只能通过提取激光中心线获取这一条线上的高度信息,那么要进行三维重建的话,就需要通过平移或者是旋转的方式,来让线激光扫描被测物体的完整轮廓,也就是整个表面。激光线的密度越高还原出来的物体越细腻,但由于数据量大…