synchronized 关键字基础总结

news2024/10/6 12:22:31

synchronized 关键字

说一说你对 synchronized 关键字的理解?

synchronized 翻译成中文是同步的的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。 因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

不过,在 Java 6 之后,Java 官方从 JVM 层面对 synchronized 进行了较大的优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。

如何使用 synchronized 关键字

应用场景总结

  1. 作用于非静态方法,锁住的是对象实例(this),每一个对象实例有一个锁。
public synchronized void method() {}
  1. 作用于静态方法,锁住的是类的 Class 对象,Class 对象全局只有一份,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程。
public static synchronized void method() {}
  1. 作用于 Lock.class,锁住的是 Lock 的 Class 对象,也是全局只有一个。
synchronized (Lock.class) {}
  1. 作用于 this,锁住的是对象实例,每一个对象实例有一个锁。
synchronized (this) {}
  1. 作用于静态成员变量,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个。
public static Object monitor = new Object(); 
synchronized (monitor) {}

总结:

  1. 必须有“对象”来充当“锁”的角色。
  2. 对于同一个类来说,通常只有两种对象来充当锁:实例对象、Class 对象(一个类全局只有一份)。
    • Class 对象:静态相关的都是属于 Class 对象,还有一种直接指定 Lock.class。
    • 实例对象:非静态相关的都是属于实例对象

Class对象

在Java中,每个类在JVM中都有一个唯一的Class对象与之对应,用于描述该类的类型信息,例如类的名称、访问修饰符、父类信息、实现的接口信息等。Class对象是一个非常重要的对象,它是Java反射机制的基础,也是Java虚拟机在类加载过程中的关键数据结构之一。

Class对象的创建是在类加载的过程中完成的,当类被第一次使用时,JVM会从磁盘中将该类的字节码加载到内存中,并创建一个对应的Class对象,存储该类的类型信息。每个Class对象都对应着一个具体的类,包含了该类的所有信息,例如:类的成员变量、成员方法、构造方法、静态代码块、注解等等。

Class对象是一个非常重要的对象,它的作用远不止于反射机制的支持,还可以用于实现类加载器的缓存,动态代理、字节码增强等高级特性,因此,深入理解Class对象是Java开发中必须掌握的知识点。

最主要的三种应用场景

  1. 修饰实例方法

  2. 修饰静态方法

  3. 修饰代码块

1、修饰实例方法 (锁当前对象实例)

给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。


synchronized void method() {
    //业务代码
}

2、修饰静态方法 (锁当前类)

给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。

这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。

synchronized static void method() {
    //业务代码
}
静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态 synchronized 
方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用
的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

3、修饰代码块 (锁指定对象/类)

对括号里指定的对象/类加锁:

  • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁。
  • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
synchronized() {
    //业务代码
}

总结:

  • synchronized 关键字加到 static 静态方法和 synchronized(类.class) 代码块上都是是给 Class 类上锁;
  • synchronized 关键字加到实例方法上是给对象实例上锁;
  • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能。

构造方法可以使用 synchronized 关键字修饰么?

构造方法不能使用 synchronized 关键字修饰。

构造方法本身就属于线程安全的,不存在同步的构造方法一说。

讲一下 synchronized 关键字的底层原理

synchronized 关键字底层原理属于 JVM 层面。

synchronized 同步语句块的情况

public class TestA {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

翻译成字节码:

public method()V
    TRYCATCHBLOCK L0 L1 L2 null
    TRYCATCHBLOCK L2 L3 L2 null
   L4
    LINENUMBER 5 L4
    ALOAD 0
    DUP
    ASTORE 1
    MONITORENTER 
   L0
    LINENUMBER 6 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "synchronized \u4ee3\u7801\u5757"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L5
    LINENUMBER 7 L5
    ALOAD 1
    MONITOREXIT
   L1
    GOTO L6
   L2
   FRAME FULL [com/lzl/algorithm/test12/TestA java/lang/Object] [java/lang/Throwable]
    ASTORE 2
    ALOAD 1
    MONITOREXIT
   L3
    ALOAD 2
    ATHROW
   L6
    LINENUMBER 8 L6
   FRAME CHOP 1
    RETURN
   L7
    LOCALVARIABLE this Lcom/lzl/algorithm/test12/TestA; L4 L7 0
    MAXSTACK = 2
    MAXLOCALS = 3

从上面我们可以看出:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。

另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,
否则会抛出java.lang.IllegalMonitorStateException的异常的原因

在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

在这里插入图片描述
对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。

在这里插入图片描述
如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被拥有锁的线程释放为止。

synchronized 修饰方法的的情况

public class TestA {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}

翻译成字节码:
通过 JDK 自带的 javap 命令查看 TestA 类的相关字节码信息:首先切换到类的对应目录执行 javac TestA.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l TestA.class。

public synchronized void method();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String synchronized 鏂规硶
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。

总结

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

不过两者的本质都是对对象监视器 monitor 的获取。

JDK1.6 之后的 synchronized 关键字底层做了哪些优化?

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

  1. 优化了锁的获取和释放机制
    在JDK1.6之前,synchronized关键字的实现采用的是重量级锁(也称为互斥量),每次获取锁都需要进入内核态,这种方式效率较低。JDK1.6之后,Java引入了偏向锁、轻量级锁和重量级锁三种锁的状态,根据锁对象竞争的情况自动升级或降级锁的状态,从而提高锁的获取和释放效率。

  2. 优化了对象监视器的实现
    在JDK1.6之前,每个Java对象都会有一个监视器(monitor)与之关联,用于实现synchronized关键字的同步功能。在JDK1.6之后,Java对对象监视器的实现进行了优化,使用了一种基于管程的模型,用于实现线程的同步和互斥。这种模型将对象的监视器和对象本身分离开来,减少了锁的竞争,从而提高了程序的并发性能。

总的来说,JDK1.6之后,Java对synchronized关键字的实现进行了一系列的优化,提高了程序的并发性能和效率。但是,在多线程编程中,使用synchronized关键字还是需要谨慎,需要遵循一定的规范和约束,才能保证线程安全和程序的正确性。

优化了对象监视器的实现

在JDK1.6之前,每个Java对象都会有一个监视器(monitor)与之关联,用于实现synchronized关键字的同步功能。在JDK1.6之后,Java对对象监视器的实现进行了优化,使用了一种基于管程的模型,用于实现线程的同步和互斥。

具体来说,Java使用了一种叫做“对象内部锁”(Intrinsic Lock)的机制来实现synchronized关键字的同步功能。每个Java对象都有一个内部锁,也称为监视器锁(Monitor Lock),用于控制对该对象的并发访问。当一个线程获取了该对象的内部锁,其他线程就无法访问该对象,直到该线程释放了该对象的内部锁。

Java的对象内部锁是基于管程(Monitor)实现的。每个Java对象都与一个Monitor关联,Monitor包含了该对象的内部锁和等待队列。在Java虚拟机中,每个Monitor都有一个Owner字段,用于记录当前持有该Monitor的线程。当一个线程获取了该对象的内部锁时,该线程就成为了该Monitor的Owner,其他线程就无法获得该对象的内部锁。如果有其他线程试图获取该对象的内部锁,就会被放入该Monitor的等待队列中,等待Owner线程释放内部锁后再次竞争。

这几种优化的详细信息可以查看这篇文章——Java6 及以上版本对 synchronized 的优化。

synchronized 和 volatile 的区别

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在。

  • volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

synchronized 和 ReentrantLock 的区别

两者都是可重入锁

“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。

synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:

  • 等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • 可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • 可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。
Condition是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例
(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。
 
在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,
这个功能非常重要,而且是 Condition 接口默认提供的。

而synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。

如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,
而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

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

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

相关文章

第一眼看到就喜欢上了

有多少小伙伴是被标题 骗 吸引进来的呢,小编可不是标题党。 今天给大家推荐的是一款开源3D 博客,确实是第一眼看到就喜欢上了。 相信大家跟我一样,曾经可能花费大量的时间和精力打造过自己的专属博客。只为自己的博客看上去与众不同&#x…

2023Java商城毕业设计(附源码和数据库文件下载链接)Spring Boot + mysql + maven + mybatis-plus

2023Java商城毕业设计Spring Boot mysql maven mybatis-plus 用户注册用户登录修改密码商品列表(分类模糊查询)个人信息用户信息修改订单信息添加至购物车商品列表商铺详情商品详情商铺列表 资源目录如下:(源码sql文件&#xf…

scratch判断亲和数 中国电子学会图形化编程 少儿编程 scratch编程等级考试四级真题和答案解析2023年3月

目录 scratch判断亲和数 一、题目要求 1、准备工作 2、功能实现 二、案例分析 <

案例分享|CPU监控异常

CPU使用率监控很关键&#xff0c;综合反应系统的负载情况&#xff0c;是监控的重要指标之一。CPU的使用率&#xff0c;对业务系统性能有重要的影响&#xff0c;根据CPU使用率监控&#xff0c;可以对系统或应用进一步分析调优。 4月25日22点&#xff0c;平台收到某县级医院HIS数…

回炉重造十二----网络文件共享服务

网络文件共享服务 1、FTP文件传输协议 1.1 FTP工作原理 FTP的20和21端口的区别 20端口是用来传输数据的 21端口是客户端用来连接FTP服务器 主动模式&#xff08;PORT&#xff09;&#xff1a; 客户端连接到FTP服务端的21号端口&#xff0c;发送用户名和密码当客户端成功登…

对线面试官,JUC面试专题强化

一、AQS高频问题 1.1 AQS是什么&#xff1f; AQS是JUC下大量工具的基础类&#xff0c;很多工具都基于AQS实现的&#xff0c;比如lock锁&#xff0c;CountDownLatch&#xff0c;Semaphore&#xff0c;线程池等等都用到了AQS。 AQS中有一个核心属性state&#xff0c;还有一个…

Android 自定义View实战—制作一个简易输入框

这次我们来做一个简易输入框&#xff0c;可以用于密码输入和验证码输入。 依然在EasyView中进行创建&#xff0c;在com.easy.view下新建一个EasyEditText&#xff0c;继承自View &#xff0c;实现里面的构造方法。 ① 构造方法 然后我们继承自View&#xff0c;重写里面的构造…

一些良心软件的推荐

推荐软件一&#xff1a;硬盘规划——SpaceSniffer SpaceSniffer 是一款免费的硬盘空间管理软件&#xff0c;可以帮助你快速分析你电脑硬盘中的文件和文件夹&#xff0c;让你更加清楚地了解硬盘的使用情况。通过SpaceSniffer&#xff0c;你可以轻松地找到占用大量空间的文件和文…

Java—JDK8新特性—接口增强

目录 JDK引言 1.相关组织和规范 1.1 JCP (Java Community Process) 1.2 JSR (Java Specification Requests) 1.3 JEP (Java Enhancement Proposal) JDK8新特性 1.接口增强 1.1 默认方法 1.1.1 为什么引入默认方法 1.1.2 如何使用默认方法 1.1.3 如何调用默认方法 1…

用Radare2模拟shellcode运行

当我们在编写汇编时&#xff0c;可能有的时候你需要看看编译器中到底发生了什么。如果你正在排除shellcode出现的问题&#xff0c;你那么更需要耐心地、慎重地运行指令。 本文将探讨如何在x86_64的Ubuntu系统上模拟32位ARM shellcode。由于大多数笔记本电脑和工作站还没有运行…

单篇笔记涨粉8w,10秒视频播放超1000w,小红书最近在流行什么?

四月&#xff0c;小红书平台又涌现出哪些优质博主&#xff1f;品牌在投放种草方面有何亮眼表现&#xff1f; 为洞察小红书平台的内容创作趋势及品牌营销策略&#xff0c;新红推出4月月度榜单&#xff0c;从创作者及品牌两方面入手&#xff0c;解析月榜数据&#xff0c;为从业者…

iOS总结_UI层自我复习总结

UI层复习笔记 在main文件中&#xff0c;UIApplicationMain函数一共做了三件事 根据第三个参数创建了一个应用程序对象 默认写nil&#xff0c;即创建的是UIApplication类型的对象&#xff0c;此对象看成是整个应用程序的一个抽象&#xff0c;负责存储应用程序的状态。根据第四…

SpringBoot访问静态资源

SpringBoot项目中没有WebApp目录&#xff0c;只有src目录。在src/main/resources下面有static和templates两个文件夹。SpringBoot默认在static目录中存放静态资源&#xff0c;而templates中放动态页面。 static目录 SpringBoot通过/resources/static目录访问静态资源&#xff…

怎么衡量纸白银走势图的强弱?

目前国内银行提供的纸白银交易基本实现了全天候连续的交易时间&#xff0c;但由于银行所提供的交易终端的技术分析功能有限&#xff0c;投资者在分析行情时绑手绑脚&#xff0c;因此小编建议大家可以尝试使用国际上主流的MT4的平台&#xff0c;作为观察国际银价走势的参考和技术…

在 Python 中获取昨天的日期

文章目录 在 Python 中获取昨天的日期Python 中的Date模块 在 Python 中获取昨天日期的示例 我们将通过多个示例介绍如何使用 Python 获取昨天的日期。 在 Python 中获取昨天的日期 Python 是一种高级语言&#xff0c;可用于数据科学和机器学习&#xff0c;以使用 Python 的数…

unity进阶学习笔记:消息框架

1 使用消息框架的目的 对于小型游戏&#xff0c;可能不需要任何框架&#xff0c;而是让各个游戏脚本直接相互通信。如要实现玩家受到攻击血量减少&#xff0c;通过玩家控制类向血条脚本发送消息减少血量。但是这样直接通信会导致各脚本通信关系记为复杂&#xff0c;并且每一个…

测试5年从中兴 15K 跳槽去腾讯 32K+16,啃完这份笔记你也可以

粉丝小王转行做测试已经是第5个年头&#xff0c;一直是一个不温不火的小职员&#xff0c;本本分分做着自己的事情&#xff0c;觉得自己的工作已经遇到了瓶颈&#xff0c;一个偶然的机会&#xff0c;获得了一份软件测试全栈知识点学习笔记&#xff0c;通过几个月的学习&#xff…

基于AT89C51单片机的计算器设计与仿真

点击链接获取Keil源码与Project Backups仿真图: https://download.csdn.net/download/qq_64505944/87759134?spm=1001.2014.3001.5503 源码获取 主要内容: 本次设计所提出的一种基于单片机技术的简易计算器的方案,能更好的解决计算机计算的问题,随着数字生活的到来,单片…

什么是皮安计?皮安表测试测量软件分享NS-SourceMeter

什么是皮安计 测量低直流电流&#xff0c;其需求常常远远超出数字万用表的功能。一般来说&#xff0c;数字万用表缺少测量低于100nA的电流所需的灵敏度。即使在较高的电流水平上&#xff0c;一个DMM的输入电压降&#xff08;电压负担&#xff09;高达几百毫伏&#xff0c;也不…

记录线上排查内存泄露问题

背景 记录一次云上排查内存泄露的问题&#xff0c;最近监控告警云上有空指针异常报出&#xff0c;于是找到运维查日志定位到具体是哪一行代码抛出的空指针异常&#xff0c; 问题分析 发现是在解析cookie的一个方法内&#xff0c;调用HttpServletRequest.getServerName()获取…