• 【Java】【多线程】synchronized 关键字


    synchronized

    一、synchronized用法

    • volatile关键字可以保证共享变量的可见性和有序性,但并不能保证原子性。
    • 既保证共享变量的可见性和有序性,又保证原子性,synchronized关键字。
    • 作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。
    • 修饰 实例方法、静态方法、代码块
    public synchronized void add(){...}
    public static synchronized void add(){...}
    public void add() { synchronized (this) {...} }
    
    • 1
    • 2
    • 3

    二、Java对象头与Monitor对象

    对象在JVM内存中的存储分为三个区域:对象头、实例数据以及填充数据。
    对象头: 是实现synchronized的锁对象的基础。
    实例数据: 存放类的属性数据信息,包括父类的属性信息,这部分内存按4字节对齐
    填充数据: 由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

    2.1 对象头
    • 以Hotspot虚拟机为例,对象头主要包括三部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)和数组长度(只有数组对象有)。
    • JVM中采用2个字来存储对象头,如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度。
    • Mark Word 用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
    • Klass Point 是对象指向它的类元数据的指针,来确定这个对象是哪个类的实例。
    • 数组长度是数组对象特有的部分,该数据在32位和64位JVM中长度都是32bit。
    2.2 Mark Word(标记字段)
    • Mark Word 用于存储对象自身的运行时数据,同时记录了对象和锁有关的信息。
    • Mark Word在不同的锁状态下存储的内容不同:
      在这里插入图片描述

    (1)当没有被当成锁时,就是普通的对象,锁标志位是01,是否偏向锁那一位是0。
    (2)当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1。
    (3)当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,线程id相同,表示线程A已经获得了这个偏向锁。
    (4)当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,线程B会先用CAS操作试图获得锁,成功改为线程B的id,抢锁失败,则继续执行步骤5。
    (5)偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。抢到了同步锁,就把锁标志位改成00。
    (6)轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。
    (7)自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

    2.3 CAS 操作

    CAS(Compare And Swap)操作,也叫做比较与交换,是一种无锁原子算法。

    • 使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。
    • CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。
    • 出现冲突就重试当前操作直到没有冲突为止。

    三、monitor对象

    • 重量级锁synchronized的标识位是10,Mark Word存储了指向堆中的Monitor对象的指针。
    • monitor对象,也称为管程或监视器锁。
    • 每一个对象实例都会关联一个Monitor对象。
    • 这个Monitor对象既可以与对象一起创建销毁,也可以在线程试图获取对象锁时自动生成。
    • 当这个Monitor对象被线程持有后,它便处于锁定状态。
    • 过程:
      (1) 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
      (2) 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
      (3) 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

    四、等待/唤醒 可重入

    • 等待唤醒机制主要指的是notify/notifyAll和wait方法。
    • 调用这几个方法前必须拿到当前对象的监视器monitor对象。
    • 在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常。
    • synchronized的可重入性:
      (1) 从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态。
      (2) 但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。
      (3) synchronized是基于原子性的内部锁机制,是可重入的。
      (4) 在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

    五、JVM对synchronized的优化

    • 在Java早期版本中,synchronized属于重量级锁,效率低下。因为在实现上,JVM会阻塞未获取到锁的线程,直到锁被释放的时候才唤醒这些线程。
    • 阻塞和唤醒操作是依赖操作系统来完成的,需要从用户态切换到内核态,开销很大。
    • Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。
    • 锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
    • 随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,但是锁的升级是单向的,只能从低到高升级,不会出现锁的降级。
    5.1偏向锁
    • 在大多数情况下锁不仅不存在多线程竞争关系,而且大多数情况都是被同一线程多次获得。为了减少同一线程获取锁的代价而引入了偏向锁的概念。
    • 如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构。
    • 当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提升了程序的性能。
    • 对于锁竞争比较激烈的情况,会升级为轻量级锁。
    5.2轻量级锁
    • 对于大部分的锁,在整个同步生命周期内都不存在竞争。 适应线程交替执行同步块的的场景.
    • 如果存在同一时间访问同一锁的场合,就会导致轻量级锁就会失效,进而膨胀为重量级锁。
    5.3自旋锁
    • 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
    • 基于在大多数情况下,线程持有锁的时间都不会太长。
    • 自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),不断的尝试获取锁。

    参考
    https://blog.csdn.net/u012723673/article/details/102681942
    https://zhangpan.site/2021/06/14/39-synchronized/

  • 相关阅读:
    pdf暴力解除密码,忘记pdf密码怎么找回?
    Linux下载及配置
    SVM之SVR参数详解以及调参
    1024程序员节献礼,火山引擎ByteHouse带来三重产品福利
    反射和注解
    Vitis_米联客开发板MZU07_7EG上手_1
    OpenHarmony 3.1 Beta 版本关键特性解析——ArkUI canvas组件
    Java:基础语法2
    【Django项目记录:django admin后台将查询结果导出表格】
    Windows环境下下载安装Elasticsearch和Kibana
  • 原文地址:https://blog.csdn.net/RiceVan/article/details/126950584