Synchronized详解

java中每一个对象都可以做为锁,具体表现为以下三种形式:

  • 对于普通方法,锁的是当前实例对象;
  • 对于静态方法,锁的的当前类的Class对象;
  • 对于同步代码块;锁的是synchronized括号中配置的对象。

Synchonized在JVM里的实现原理是,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但是实现细节不同。

  • 同步方法

    同步方法,JVM通过ACC_SYNCHRONIZED标记符来实现同步。

    方法级别的同步是隐式的同步方法的常量池中会有一个ACC_SYNCHRONIZED标志,当某个线程要访问某个方法时,会检查是否有ACC_SYNCHRONIZED,如果有,则需要先获取到监视器锁,然后才能开始执行方法,执行完了释放监视器锁,在这期间,其他线程来请求执行该方法,会因为监视器锁无法获取而阻塞;并且在此期间,如果执行方法过程中发生异常,并且该方法内未处理该异常,那么抛出异常到方法外面时,该线程会释放监视器锁

  • 代码块

    代码块的同步是使用monitorentermonitorexit指令实现的。monitorenter指令是在编译后插入到同步代码块的开始位置,而**monitorexit是插入到代码块结束处和异常处**, JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联, 当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

Java对象头

Hotspot虚拟机的对象头主要包括两部分:Mark Word(标记字段)、Klass Pointer(类型指针)

  • Klass Point 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

  • Mark Word 用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键,所以下面将重点阐述 Mark Word 。

    • Mark Word用于存储运行时数据,哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。

    • 对象头一般占有两个机器码,32位虚拟机中,1机器码=4字节=32bits

      25Bit4bit1bit2bit
      对象的hashCode对象的分代年龄是否是偏向锁锁标志位

      在这里插入图片描述

Monitor

每一个Java对象都带有一把看不见的锁,内部锁或Monitor锁

Monitor Record 是线程私有的数据结构,每一个线程都有一个可用 Monitor Record 列表,同时还有一个全局的可用列表。 每一个被锁住的对象都会和一个 Monitor Record 关联(对象头的 MarkWord 中的 LockWord 指向 Monitor 的起始地址),Monitor Record 中有一个 Owner 字段,存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

锁的升级与优化

为什么优化?

在JVM中,monitorenter和monitorexit字节码都是依赖于底层操作系统的Mutex Lock实现的,需用将当前线程挂起,并从用户态切换到内核态,这个过程的代价非常昂贵,然后实际情况是,很多时候同步方法都运行在无锁竞争的单线程的情况下,每次再调用操作系统就得不偿失了。

自旋锁

线程的阻塞和唤醒,需要 CPU 从用户态转为核心态。频繁的阻塞和唤醒对 CPU 来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时,我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间。为了这一段很短的时间,频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。

自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。

自旋等待不能替代阻塞,先不说对处理器数量的要求,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,这样反而会带来性能上的浪费。

所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。自旋的默认次数为 10 次,可以通过参数 -XX:PreBlockSpin 来调整。

适应自旋锁

自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?

  • 线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。
  • 反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

锁消除

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制。但是,在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。

锁消除的依据是逃逸分析的数据支持,因为虽然没有显示使用锁,但是我们在使用一些 JDK 的内置 API 时,如 StringBuffer、Vector、HashTable 等,这个时候会存在隐性的加锁操作

锁粗化

锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁

锁的升级

​ 锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

  • 重量级锁

    重量级锁通过对象内部的监视器(Monitor)实现。其中,Monitor 的本质是,依赖于底层操作系统的 Mutex Lock 实现。操作系统实现线程之间的切换,需要从用户态到内核态的切换,切换成本非常高。

  • 轻量级锁

    引入轻量级锁的主要目的,是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

    当关闭偏向锁功能或者多个线程竞争偏向锁,导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:

    • 判断当前对象是否处于无锁状态?若是,则 JVM 首先将在当前线程的栈帧中,建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word的 拷贝(官方把这份拷贝加了一个 Displaced 前缀,即 Displaced Mark Word);否则,执行步骤(3);
    • JVM 利用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指正。如果成功,表示竞争到锁,则将锁标志位变成 00(表示此对象处于轻量级锁状态),执行同步操作;如果失败,则执行步骤(3);
    • 判断当前对象的 Mark Word 是否指向当前线程的栈帧?如果,则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则,只能说明该锁对象已经被其他线程抢占了,当前线程便尝试使用自旋来获取锁。若自旋后没有获得锁,此时轻量级锁会升级为重量级锁,锁标志位变成 10,当前线程会被阻塞。
    释放锁
    • 取出在获取轻量级锁保存在 Displaced Mark Word 中 数据。
    • 使用 CAS 操作将取出的数据替换当前对象的 Mark Word 中。如果成功,需要唤醒被挂起的线程,则说明释放锁成功;否则,执行(3)。
    • 如果 CAS 操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。
  • 偏向锁

    大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

    • 获取过程

      当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID

      以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。

      如果测试成功,表示线程已经获得了锁。

      如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):

      如果没有设置,则使用CAS竞争锁,竞争成功,则将 Mark Word 的线程 ID 替换为当前线程 ID

      如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁

    • 释放过程

      首先会暂停拥有偏向锁的线程并检查该线程是否存活

      1. 如果线程非活动状态,则将对象头设置为无锁状态(其他线程会重新获取该偏向锁)。
      2. 如果线程是活动状态,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,并将对栈中的锁记录和对象头的 MarkWord 进行重置
        • 要么重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁)
        • 要么恢复到无锁或者标记锁对象不适合作为偏向锁(此时锁会被升级为轻量级锁)

      最后唤醒暂停的线程,被阻塞在安全点的线程继续往下执行同步代码块

对比

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法,仅存在ns级的差距如果线程间存在竞争,会带来额外的锁撤销消耗只有一个线程访问同步块的场景
轻量级锁竞争的线程不会阻塞,提高程序响应速度如果始终获取不到锁的线程会使用自旋锁消耗CPU同步代码块执行时间很快
重量级锁线程竞争不会使用自旋锁,不会消耗CPU。线程阻塞,响应时间慢追求吞吐量,同步代码块执行时间较慢
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值