【并发】Synchronized的底层原理

发布于:2024-06-05 ⋅ 阅读:(129) ⋅ 点赞:(0)

基本概念

Synchronized【对象锁】采用互斥的方式让同一时刻最多只有一个线程能够持有【对象锁】,如果其他线程想要获取这个【对象锁】就会被阻塞住

底层实现原理

我们可能听过,synchronized底层是通过Monitor来实现的,但如何直观的观察呢?可以通过反汇编来看一下,以下是一段简单的加锁代码

我们通过java -v 文件名.class 来反汇编一下

 可能会疑问为什么会释放两遍锁呢?是因为防止第一遍释放锁的时候会报错,导致死锁的问题,为了以防万一所以设置的释放两次锁

Monitor

Monitor被翻译成监视器,是由JVM提供的,由C++来实现,它的结构如下

当线程到来的时候,线程首先会判断Monitor中的Owner是否为空,如果为空则当前线程就会持有锁,如果不为空,那么当前线程会进入导EntryList中进行等待,此时里面的线程出于Blocked的状态,当Owner为空的时候,这些阻塞的线程就会争抢Owner;而其中的WaitSet是存储的调用wait方法的等待的线程,就是处于Waiting状态的线程

以上所说的都是浅层的Synchronized的底层原理,更深层此的还要继续往下面看

Synchronized的锁升级

Monitor实现的锁属于重量级锁,涉及到用户态(java代码)和内核态(CPU层面)的切换、进程上下文的切换,成本比较高,性能比较低

所以在JDK1.6 引入了两种新型的锁机制:偏向锁轻量级锁,他们的要引入是为了解决在没有多线程竞争或者基本没有竞争的场景下重量级锁机制带来的性能开销

要了解锁升级的流程,我们就需要先了解一下对象的内存结构

对象的内存结构

在HotSpot虚拟机中,对象在内存存储的布局可以分为三个区域,对象头、实例数据和对齐填充。其中对象头对于锁升级的关系是很大的

对象头的结构分为32位和64位,这里我们主要是介绍32位的对象头

  • hashcode:25位的对象标识Hash码
  • age:对象分代年龄占4位
  • biased lock:偏向锁标识,占1位,0表示没有开始偏向锁,1表示开启了偏向锁
  • thread:持有偏向锁的线程ID,占23位
  • epoch:偏向时间戳,占2位
  • ptr to lock record:轻量级锁状态下,指向栈中锁记录的指针,占30位
  • ptr to heavyweight monitor:重量级锁状态下,指向对象监视器Mo nitor的指针,占30位

对象是如何关联Monitor

每个Java对象都可以关联一个Monitor对象,让如果使用synchronized给对象上锁(重量级锁)之后,该对象头的Mark Word就被设置只想Monitor对象的指针,就是上文我们所提到的ptr to heavyweight monitor

轻量级锁

在很多的情况下,在Java程序运行的时候,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步代码块。这种情况下,用重量级锁是没有必要的,所以JVM就引入了轻量级锁

如上述代码,obj这个对象获取了方法一和方法二两个方法的锁,如果现在来了一个线程调用了method1,之后又进入了method2,此时当前线程就进入了同样一把锁两次(重入性),由于是同一个线程所持有的锁,所有就没有必要出现锁的竞争,所以可以使用轻量级锁

下面我们介绍一下轻量级锁的执行流程

 加锁流程

  1. 在线程栈中创建一个Lock Record,将其obj字段指向锁对象
  2. 通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁的状态则修改成功,代表该线程获取到了轻量级锁
  3. 如果是当前线程已经持有来该锁了,代表这是一次锁的重入。设置Locak Record第一部分位null,起到一个重入计数器的作用
  4. 如果CAS失败,说明发生了竞争,此时就需要转换为欸重量级锁

解锁流程

  1. 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record
  2. 如果Lock Record的Mark Word为空,代表这是一次可重入,将obj设置为空后继续
  3. 如果Lock Record的Mard Word不为空,则利用CAS指令将对象头中的Mark Word恢复成无锁的状态,如果失败,则膨胀为重量级锁

偏向锁 

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有,由此可以知道偏向锁的性能比轻量级锁的性能还要好

我们可以看一下偏向锁的内存结构:

它是由biased lock偏向锁标识,占1位,0表示没有开始偏向锁,1表示开启了偏向锁来表示的

下面我们介绍一下偏向锁的执行流程

锁是如何升级的呢

Java中synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有、多个线程竞争锁的三种情况了

所以一旦锁发生了竞争,锁就会升级为重量级锁