深入浅出synchronized关键字

news2024/11/17 1:47:23

前言

无论在日常工作还是面试过程中,synchronized关键字作为并发场景下的操作,是一定要掌握的,本文从synchronized的使用方式、原理及优化三个方面,对synchronized关键字作一个系统化的说明。

使用方式

synchronized主要有三种使用方式,分别是

  • 修饰普通方法,锁作用于当前对象
  • 修饰静态方法,锁作用于类
  • 修饰代码块,锁作用于当前对象实例,需要指定加索的对象

1、修饰普通方法

当synchronized关键字加到普通方法上时,这个方法就被加上了同步锁,这也使得某一时间只有一个线程可以访问该方法。

package com.example.dailyrecords.demo;

import java.util.Date;

/**
 * @author zy
 * @version 1.0.0
 * @ClassName synchronizedTest.java
 * @Description TODO
 * @createTime 2022/12/22
 */
public class synchronizedTest {
    public synchronized void test(){
        try{
            System.out.println(Thread.currentThread().getName()+new Date());
            Thread.sleep(5000);
            System.out.println("结束"+new Date());
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        //初始化
        synchronizedTest synchronizedTest = new synchronizedTest();
        for (int i = 1; i < 3; i++) {
            new Thread(()->{
                synchronizedTest.test();
            },String.valueOf(i)+"号线程").start();
        }
    }
}

在这里插入图片描述

上面定义了一个普通方法test,然后开启两个线程去执行test方法,由于加上了synchronized关键字,因此,某一时间只有一个线程获取到锁并执行,另一个线程被阻塞,直到获取锁的线程释放锁,另一个才执行。
在这里插入图片描述

2、修饰静态方法

由于静态方法是在类初始化的时候加载的,因此synchronized关键字也就在类初始化时作用到了当前类对象上,因此锁住的是整个类。

package com.example.dailyrecords.demo;

import java.util.Date;

/**
 * @author zy
 * @version 1.0.0
 * @ClassName synchronizedTest.java
 * @Description TODO
 * @createTime 2022/12/22
 */
public class synchronizedTest {
    public static synchronized void test(){
        try{
            System.out.println(Thread.currentThread().getName()+new Date());
            Thread.sleep(50000);
            System.out.println("结束"+new Date());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    

    public static void main(String[] args){
        for (int i = 1; i < 3; i++) {
            new Thread(()->{
                test();
            },String.valueOf(i)+"号线程").start();
        }
    }
}

在这里插入图片描述

3、修饰代码块

synchronized的锁的粒度能不能更小呢?那就是锁住一块代码块,如下所示

public void test1(){
        synchronized (this){
            //代码块
        }
    }

这里锁住的就是括号里面的内容,这里的synchronized (this),代表着只有当前对象才可以访问这段代码。

原理

synchronized作为一个关键字,它的底层是通过monitor监视器锁实现的。我们先将代码编译得到字节码文件,javac xxx.java,然后通过javap -v xxx.class查看字节码命令,如下

public class com.example.dailyrecords.demo.synchronizedTest2
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#21         // java/lang/Object."<init>":()V
   #2 = Class              #22            // com/example/dailyrecords/demo/synchronizedTest2
   #3 = Methodref          #2.#21         // com/example/dailyrecords/demo/synchronizedTest2."<init>":()V
   #4 = Methodref          #2.#23         // com/example/dailyrecords/demo/synchronizedTest2.method1:()V
   #5 = Methodref          #2.#24         // com/example/dailyrecords/demo/synchronizedTest2.method2:()V
   #6 = Class              #25            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               method1
  #12 = Utf8               method2
  #13 = Utf8               StackMapTable
  #14 = Class              #22            // com/example/dailyrecords/demo/synchronizedTest2
  #15 = Class              #25            // java/lang/Object
  #16 = Class              #26            // java/lang/Throwable
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               SourceFile
  #20 = Utf8               synchronizedTest2.java
  #21 = NameAndType        #7:#8          // "<init>":()V
  #22 = Utf8               com/example/dailyrecords/demo/synchronizedTest2
  #23 = NameAndType        #11:#8         // method1:()V
  #24 = NameAndType        #12:#8         // method2:()V
  #25 = Utf8               java/lang/Object
  #26 = Utf8               java/lang/Throwable
{
  public com.example.dailyrecords.demo.synchronizedTest2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 10: 0

  public synchronized void method1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 13: 0

  public void method2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_1
         5: monitorexit
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     6     9   any
             9    12     9   any
      LineNumberTable:
        line 15: 0
        line 17: 4
        line 18: 14
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ class com/example/dailyrecords/demo/synchronizedTest2, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/example/dailyrecords/demo/synchronizedTest2
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method method1:()V
        12: aload_1
        13: invokevirtual #5                  // Method method2:()V
        16: return
      LineNumberTable:
        line 21: 0
        line 22: 8
        line 23: 12
        line 24: 16
}

可以看到,method2()方法加上synchronized关键字之后,添加了monitorenter和monitorexit命令,monitorenter存在于同步代码块开始的位置,而monitorexit存在于同步代码块结束的位置,它们分别代表着获取锁和释放锁。每一个monitorenter都必须对应一个monitorexit。
在这里插入图片描述
monitor主要由计数器count、阻塞线程集合_EntryList、释放锁线程集合_WaitSet和持有锁_Owner构成,当没有线程获取这把锁的时候,count值为0,如果有一个线程获取这把锁,它的值就会+1,并且设置该线程为锁的持有者。_owner指向的就是当前持锁线程。如果该线程已经占用该锁,并且重新进入,那么count的值就会+1。当执行到monitorexit的时候,count的值就会-1,直到count值为0的时候,该持锁线程会进入到WaitSet里面,将状态改为等待状态,让其他处于EntryList里的阻塞线程重新自旋获取这把锁。

锁的优化

这里主要对锁升级做一些说明,可能大家也都了解自旋锁、偏向锁、轻量级锁和重量级锁这个升级过程,下面详细说明。

自旋锁

当一个线程在获取锁的时候,如果该锁已被其它线程获取到,那么该线程就会去循环自旋获取锁,不停地判断该锁是否能够已经被释放,自选直到获取到锁才会退出循环。通常该自选在源码中都是通过for(; ;)或者while(true)这样的操作实现,但是如果一直自旋下去,也会造成CPU资源的浪费,因此,当自旋次数超过一定次数后,这个线程就会被挂起。

偏向锁(可重入锁)

首先,一个对象在内存中由对象头、示例数据和数据填充三部分组成。当一个线程获取锁之后,这个锁对象在对象头中就会记录这个线程的ID,之所以偏向就是因为有这个ID,如果后面还是这个线程进入和退出同步时,只要检查是否是这个偏向的线程ID即可。

轻量级锁

由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,底层通过自旋来实现,并不会阻塞线程,需要强调的是,轻量级锁并不是用来代替重量级锁的。引入轻量级锁的目的在于:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁。

重量级锁

如果自旋多次仍然没能获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞,除了持有锁的线程其他全部阻塞,防止CPU空转

锁升级

synchronized锁升级是同过修改对象头来实现的
偏向锁:线程1第一次进入同步代码块的线程,将对象头的线程id修改成自己的id,此时是偏向锁,偏向锁不会自动释放锁,以后线程1再次请求就无需加锁解锁
轻量锁:线程2要竞争锁对象,而因为偏向锁不会自动释放锁,因此对象头的线程id还是线程1的id,此时需要需要查看线程1是否存活,通过cas来判断
    若不存活,锁对象置为无锁状态,线程2竞争锁设置为偏向锁;
    若存活,查看线程1的栈帧信息,若需要继续持有这个锁对象,那么暂停线程1,撤销偏向锁,升级为轻量级锁;若不需要持有,那么将锁对象设为无锁状态,重新偏向线程
重量锁:如果自旋次数到达且线程1还没有释放锁,又或者一直自旋,此时又有其他线程来竞争锁,轻量锁就会膨胀为重量锁,重量锁会将未获取到锁的线程阻塞,防止CPU空转

锁消除

在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁,例如下面代码片段

public void method() {
        synchronized (new Object()) {
            //代码逻辑

        }
    }

这段代码里面,我们new了一个Object对象来作为锁对象,但是这个对象也只有在method( )中被使用,其完整的生命周期都在这个方法中,也就是说JVM在经过逃逸分析后会对它进行栈上分配,由于在底层变成了私有数据,那么也就无需加锁了。

锁粗化

在JIT编译时,发现如果有一段代码中频繁的加锁释放锁,会将前后的锁合并为一个锁,避免频繁加锁释放锁,例如下面的代码片段

public void method() {
        for(int i = 0;i < 100; i++) {
            synchronized (new Object()) {
            //代码逻辑
            
            }
        }
    }

如果按照正常的synchronized步骤走,这个循环需要进行多次的加锁解锁操作,当这段代码在即时编译时,JVM检测到每一次都是对同一个对象加锁,那么就会把这一串连续频繁的加锁解锁操作优化成仅仅一次公共的加锁解锁操作。

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

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

相关文章

Apache DolphinScheduler 发布 3.1.2 版本,Python API 实现优化

点亮 ⭐️ Star 照亮开源之路https://github.com/apache/dolphinscheduler近日&#xff0c;Apache DolphinScheduler 发布了 3.1.2 版本。此版本主要基于 3.1.1 版本进行了 6 处 Python API 优化&#xff0c;19 处 Bug 修复&#xff0c;并更新了 4 个文档。其中较为重要的 Bug…

HashMap1.7和1.8源码解析

1.HashMap &#xff08;1&#xff09;数据结构 在JDK1.7中&#xff0c;HashMap中的数据结构是数组单链表的组合&#xff1b;在JDK1.8中的HashMap存储结构是由数组、链表、红黑树这三种数据结构形成。 &#xff08;2&#xff09;JDK1.7中HashMap源码分析 &#xff08;2.1&am…

Portal门户常见宕机或性能低问题分析与七大解决之道

不论使用什么产品构建门户&#xff0c;只要是基于Java技术的&#xff0c;就很容易宕机或出现性能低的问题。因为Portal产品比较复杂&#xff0c;且集成的数据特别多&#xff0c;架构特别复杂&#xff0c;涉及到的数据通信特别多。宕机或性能低也通常是用户较为头疼的问题。经常…

excel文本函数应用:单元格中的数字和字母,如何判断?

文本字符是Excel中除了数字以外的另一种非常常用的数据类型&#xff0c;Excel也提供了大量的文本函数。利用这些函数我们可以用来判断字符串开头是否为数字、字符串是否同时包含了数字和英文、字符串是否包含了指定字符&#xff0c;可以用来转换英文字母的大小&#xff0c;可以…

Java基础之Stream的使用078

1. Stream 的使用 Stream 是什么&#xff1f; Stream 是数据渠道&#xff0c;用于操作数据源&#xff08;数组、集合等&#xff09;所生成的元素序列。 Java8两大最为重要的改变就是 Lambda表达式 与 Stream API&#xff0c;这两种改变的引入带来的是新的抽象方式 &#xff08…

目标检测之Fast RCNN概述

基本原理 Fast Rcnn主要步骤为 利用SR算法生成候选区域利用VGG16网络进行特征提取利用第一步生成的候选区域在特征图中得到对应的特征矩阵利用ROI pooling将特征矩阵缩放到相同大小并平展得到预测结果 相对于RCNN的优化 主要有三个改进 不再将每一个候选区域依次放入CNN网络…

【基于通道-空间注意的高分辨率锐化】

Channel–spatial attention-based pan-sharpening of very high-resolution satellite images &#xff08;基于通道-空间注意的很高分辨率卫星影像全色锐化&#xff09; 全色锐化处理旨在生成新的合成输出图像&#xff0c;其保留全色的空间细节和多光谱图像输入的光谱细节。…

【服务器端程序的演进过程】

目录 1 服务器端程序的演进过程 阶段一:静态服务器 阶段二:普通动态服务器 阶段三: 以用户共享内容为主的互联网生态 阶段四: 微服务时代(有高并发需求或特征的网站) 2 Java服务器项目分类 3 微服务概述 3.1 什么是微服务 3.2 为什么需要微服务 3.3 怎么搭建微服务项…

C#获取计算机详细的软件和硬件信息

利用System.Management提供的类可以用于读取本地计算机设备的各种数据&#xff0c;包括操作系统、软件、硬件的各种详细信息&#xff0c;内容很丰富。 System.Management的命名空间下&#xff0c;ManagementObjectSearcher类用于查询特定类型的设备&#xff0c;ManagementObjec…

转行做“程序员”很难?这里有几个建议...

“是什么&#xff1f;为什么&#xff1f;怎么样&#xff1f;”的灵魂三连问在我们生活中比比皆是&#xff0c;目的是为了清晰思考和看到事物的本质。对于编程学习也是一样&#xff0c;需要带着疑问从本质上去学习编。 本人是某985高校的本硕连读&#xff0c;非计算机科班出身&…

利器 | AppCrawler 自动遍历测试实践(三):动手实操与常见问题汇总

1080469 14.7 KB 上两篇文章介绍了自动遍历的测试需求、工具选择和 AppCrawler 的环境安装、启动及配置文件字段基本含义&#xff0c;这里将以实际案例更加细致的说明配置文件的用法和一些特殊场景的处理。 下面我们继续之前的例子&#xff0c;在雪球搜索框输入搜索内容后的页面…

CloudFlare系列--功能介绍与常用配置

原文网址&#xff1a;CloudFlare系列--功能与特性的介绍_IT利刃出鞘的博客-CSDN博客 简介 本文介绍CloudFlare的功能与常用的配置。 功能介绍 CloudFlare是世界最强的网络服务商。它可以提供如下服务&#xff1a; 防御DDoS攻击 世界最强防御DDos攻击的厂商。域名注册 世界最…

JavaWeb语法四:多线程案例

目录 1.单例模式 1.1&#xff1a;饿汉模式 1.2&#xff1a;懒汉模式 2.阻塞式队列 2.1:生产者消费者模型 2.2&#xff1a;阻塞队列的模拟实现 3.线程池 3.1&#xff1a;标准库中的线程池 3.2&#xff1a;模拟实现线程池 前言&#xff1a;前一篇我们讲了线程不安全的原因…

SAP UI5 里 FlexBox 的使用方法

ScrollContainer 的使用方式&#xff1a; ScrollContainer 是一个控件&#xff0c;可以在有限的屏幕区域内显示任意内容&#xff0c;并提供滚动以使所有内容都可访问。注意&#xff0c;为了避免影响用户使用体验&#xff0c;不要嵌套沿相同方向滚动的滚动区域。例如&#xff…

【LaTex】基础语法框架快速入门教程——Tex live+TexStudio简要安装及使用教程

0. 引言 LaTeX对于论文排版有着巨大的便利&#xff0c;并且对于参考文献的引用也十分方便&#xff0c;不会出现使用word引用参考文献一旦更改文献引用顺序&#xff0c;就必须全部改编号的情况。这里记录一下如何从0开始学习使用LaTeX书写论文。 1. 软件安装&环境配置 1.…

Qt中实例化一个函数变量时加不加括号的区别,以及括号中的this的使用

一、设计一个测试小程序 废话不多说&#xff0c;直接上代码。 main.h函数就不多说了&#xff0c;没改动。直接上mainwindow.h&#xff0c;也没改动。看mainwindow.cpp的内容。 #include "mainwindow.h" #include "ui_mainwindow.h" #include "test.…

机器学习算法基础——KNN算法

KNN (K-Nearest Neighbor)–K近邻分类算法 • 为了判断未知实例的类别&#xff0c;以所有已知类别的实例作为参照选择参数K • 计算未知实例与所有已知实例的距离 • 选择最近K个已知实例 • 根据少数服从多数的投票法则(majority-voting)&#xff0c;让未知实例归类为K个最邻…

Zerobot僵尸网络出现了新的漏洞利用和功能

©网络研究院 Zerobot DDoS僵尸网络已经获得了重大更新&#xff0c;扩展了其针对更多互联网连接设备和扩展网络的能力。 微软威胁情报中心 (MSTIC)正在以DEV-1061的名称跟踪持续的威胁&#xff0c;名称为未知、新兴或发展中的活动群集。 本月早些时候&#xff0c;Fort…

2022年山东省职业院校技能大赛中职组“网络安全”赛项规程

2022年山东省职业院校技能大赛中职组“网络安全”赛项规程一、赛项名称赛项名称&#xff1a;网络安全英文名称&#xff1a;Cyber Security赛项组别&#xff1a;中职组赛项类别:电子与信息类二、竞赛目的网络空间已经成为陆、海、空、天之后的第五大主权领域空间&#xff0c;习总…

SpringCloud 网关组件 Zuul-1.0 原理深度解析

为什么要使用网关&#xff1f; 在当下流行的微服务架构中&#xff0c;面对多端应用时我们往往会做前后端分离&#xff1a;如前端分成 APP 端、网页端、小程序端等&#xff0c;使用 Vue 等流行的前端框架交给前端团队负责实现&#xff1b;后端拆分成若干微服务&#xff0c;分别…