【JavaEE】synchronized 原理

news/2024/5/17 18:18:06 标签: java-ee, java, 多线程, synchronized

在这里插入图片描述

文章目录

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

前言

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

synchronized__5">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 可以安全地消除该代码块的同步锁操作。

java">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. 锁粗化

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

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

java">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次,但是每次循环都需要进行加锁和解锁的操作,这样会浪费大量的资源,所以这个代码就可以进行锁粗化的优化操作。

java">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.niftyadmin.cn/n/5066298.html

相关文章

从您输入网站 URL 到其在屏幕上完成加载的整个过程

以下是从输入网站 URL 到其在屏幕上完成加载的整个过程&#xff1a; 1. 用户输入网站 URL&#xff08;例如&#xff0c;http://www.example.com&#xff09;并按下“Enter”键。 2. 网络协议根据 URL 中的信息确定要访问的服务器 IP 地址。 3. 网络协议建立与该服务器的 TCP 连…

【LeetCode热题100】--114.二叉树展开为链表

114.二叉树展开为链表 方法一&#xff1a;对二叉树进行先序遍历&#xff0c;得到各个节点被访问到的顺序&#xff0c;利用数组存储下来&#xff0c;然后在先序遍历之后更新每个节点的左右节点的信息&#xff0c;将二叉树展开为链表 /*** Definition for a binary tree node.* …

【m_listCtrl !=NULL有多个运算符与操作数匹配】2023/9/21 上午11:03:44

2023/9/21 上午11:03:44 m_listCtrl !=NULL有多个运算符与操作数匹配 2023/9/21 上午11:04:00 如果您在编译或运行代码时遇到"M_listCtrl != NULL有多个运算符与操作数匹配"的错误提示,这通常是由于以下几个原因之一: 错误使用运算符:在条件判断语句中,应该使…

C# 图片按比例进行压缩

1、对图片进行压缩&#xff0c;保存在本地 对于一个200k的png文件按0.6的缩放比例进行压缩&#xff0c;压缩后的大小为20k左右 对于一个80k的jpg文件按0.6的缩放比例压缩&#xff0c;压缩后为13k左右 public void imageZoom(string name, Double zoomScale){Bitmap btImage …

【C++类和对象】:构造函数、析构函数、拷贝构造函数、赋值运算符重载

【C类和对象】&#xff1a;构造函数、析构函数、拷贝构造函数、赋值运算符重载 一、构造函数1.1 概念1.2 性质1.3 实例 二、析构函数2.1 概念2.2 性质2.3 实例 三、拷贝构造函数3.1 概念3.2 性质3.3 实例 四、赋值运算符重载4.1 运算符重载4.2 2 赋值运算符重载1. 赋值运算符重…

Python搭的学生信息管理系统

import os.path filenamestudent.txt def main():while True:menu()choiceint(input(请输入您要用的功能))if choice in [0,1,2,3,4,5,6,7]:if choice0:answerinput(您确定要退出系统吗&#xff1f;)if answery or answerY:print(谢谢您的使用)breakelse:continueelif choice1:…

【C语言】函数的定义、传参与调用(二)

&#x1f497;个人主页&#x1f497; ⭐个人专栏——C语言初步学习⭐ &#x1f4ab;点击关注&#x1f929;一起学习C语言&#x1f4af;&#x1f4ab; 目录 导读&#xff1a; 1. 函数的嵌套调用 1.1 什么是嵌套调用 1.2 基础实现 1.3 调用流程解析 2. 函数的链式访问 2.1 …

数据源报表

1.新建报表 2.新建数据集 3.维护数据源 支持的数据库还是蛮多哈 4.选择数据源表 5.编写sql 编码&#xff1a;SQL数据集的标识 注&#xff1a;避免特殊字符和_名称&#xff1a;SQL数据集的名称是否集合&#xff1a;否为单数据&#xff1b;是为多数据列表&#xff0c;如果多条数据…