【JavaEE】synchronized 原理

简介: 【JavaEE】synchronized 原理

bc039df3795f4241b0f7371b937b5e77.gif

前言

前面我们学习了关于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 实现原理 是什么?

本博客所写的内容


相关文章
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
关系型数据库 数据库 PostgreSQL
深入理解 PostgreSQL 的 JOIN 连接
深入理解 PostgreSQL 的 JOIN 连接
618 4
|
人工智能 数据可视化 数据挖掘
LLM代理应用实战:构建Plotly数据可视化代理
构建数据可视化代理解决了LLM(大型语言模型)在理解和生成定制图表时的局限性。代理提供DataFrame信息和自定义样式工具,简化与LLM的交互。选择了Plotly而非Matplotlib,因其交互性和Web渲染能力更适合现代可视化。代理通过元数据索引了解数据集详情,并根据样式指示生成符合特定审美的图表。通过ReActAgent和Groq模型,代理能理解用户指令,生成准确的Plotly代码,从而创建定制图表,提高了数据可视化的效率和准确性。
306 1
|
程序员 Python 容器
python 中的 collections 模块:常用数据结构和工具详解
python 中的 collections 模块:常用数据结构和工具详解
135 0
|
Oracle 关系型数据库 MySQL
入职必会-开发环境搭建17-IDEA连接数据库
IntelliJ IDEA集成了众多插件,方便开发者使用,使用IDEA自带的Database模块就可以很方便的配置、连接数据库,在 IntelliJ IDEA 中连接数据库,可以按照以下步骤进行操作。
734 0
|
机器学习/深度学习 人工智能 自然语言处理
AI百模大战:引领行业变革与开启人才黄金时代
AI百模大战:引领行业变革与开启人才黄金时代
|
运维 Serverless 流计算
Serverless 应用引擎产品使用合集之绑定自定义域名,是否可以取消验证CNAME
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。
|
JSON 安全 Java
2024年的选择:为什么Go可能是理想的后端语言
【4月更文挑战第27天】Go语言在2024年成为后端开发的热门选择,其简洁设计、内置并发原语和强大工具链备受青睐。文章探讨了Go的设计哲学,如静态类型、垃圾回收和CSP并发模型,并介绍了使用Gin和Echo框架构建Web服务。Go的并发通过goroutines和channels实现,静态类型确保代码稳定性和安全性,快速编译速度利于迭代。Go广泛应用在云计算、微服务等领域,拥有丰富的生态系统和活跃社区,适合作为应对未来技术趋势的语言。
2195 0
|
存储 PHP 开发者
PHP变量命名规则
PHP变量命名规则
238 0
|
分布式计算 安全 Hadoop
hadoop 入门 java.net.ConnectException: Connection refused
hadoop 入门 java.net.ConnectException: Connection refused
485 0