前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >多线程进阶——JUC并发编程之Synchronized底层实现概论🔥

多线程进阶——JUC并发编程之Synchronized底层实现概论🔥

作者头像
须臾之余
发布2021-12-28 10:54:11
2310
发布2021-12-28 10:54:11
举报
文章被收录于专栏:须臾之余须臾之余

Synchronized简介

Java中提供了两种实现同步的基础语义: synchronized 方法和 synchronized 块 ,先来个案例进行分析!

代码语言:javascript
复制
public class SyncTest {
    public void syncBlock(){
        synchronized (this){
            System.out.println("sync block balabala....");
        }
    }
    public synchronized void syncMethod(){
        System.out.println("sync method hahaha....");
    }
    public static void main(String[] args) {
    }
}

将SyncTest.java 编译为SyncTest.class文件,我们使用 javap -v SyncTest.class 查看class文件对应的JVM字节码信息。这里我使用的是JVM版本是JDK1.8。首先看看syncBlock()方法的字节码:

再看看syncMethod()方法的字节码:

从上面字节码可以看出,对于Synchronized 关键字而言,javac 在编译时,会生成对应的 monitorenter 和monitorexit指令,分别对应sync同步块进入和退出同步代码块,这里读者很容易发现有两个monitorexit 退出指令,原因是为了保证在程序抛出异常时最终也会释放锁

所有javac为同步代码块添加了一个隐式的try-finally,在finally中会调用 monitorexit 指令释放锁。而对于Synchronized方法而言,javac 为其生成一个 ACC_SYNCHRONIZED 关键字,在JVM进行方法调用时,发现调用的方法被 ACC_SYNCHRONIZED修饰时,则会先尝试获取锁

锁的几种形态

依赖于系统的同步函数,这些同步函数都涉及到用户态和内核态的切换、进程的上下文切换,成本较高,这使得传统意义上的锁(重量级锁)效率低下

在JDK1.6 之前,Synchronized 只有传统意义上的锁,而在JDK1.6进口了两种新型锁机制(偏向锁和轻量级锁),它们的引入是为了解决在多线程并发不高场景下使用传统锁(重量级锁)带来的性能开销问题

在了解这几种锁的实现机制之前,我们先来了解下对象头,它是多种锁机制的基础。

对象头

因为在java中任意对象都可以用作锁,因此必然需要有一个映射关系(存储该对象及其对应的锁信息),比如当前那个线程持有锁,哪些线程在等待。这就有点类似于我们学习的Map,但是如果使用Map来记录这些对应关系,需要保证Map集合的线程安全问题,

不同的Synchronized之间会相互影响,性能差,另外,当同步对象比较多时,该Map会占用比较多的内存。

why 使用对象头?因为对象头本身也有一些hashcode、GC相关数据。在JVM中,对象在内存中除了本身数据外还会有个对象头,对于普通对象而言,其对象头中有两类信息:mark work 和类型指针

另外对于数组而言还会有一份记录数组长度的数据mark work用于存储对象的hashcode 、GC分代年龄、锁状态等信息。在32位系统上 mark work长度是32bit,64位系统是64bit.为了能在有限的空间中存储更多的信息,其存储格式是不固定的,下面分别对应32bit 操作系统和64bit操作系统:

代码语言:javascript
复制
|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |状态
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |无锁态
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |偏向锁
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |轻量级锁
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |重量级锁
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |GC标记
|-------------------------------------------------------|--------------------|
代码语言:javascript
复制
|------------------------------------------------------------------------------|--------------------|
|                                  Mark Word (64 bits)                         |       State        |状态
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |无锁态
|------------------------------------------------------------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |偏向锁
|------------------------------------------------------------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |轻量级锁
|------------------------------------------------------------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |重量级锁
|------------------------------------------------------------------------------|--------------------|
|                                                                     | lock:2 |    Marked for GC   |GC标记
|------------------------------------------------------------------------------|--------------------|

可以看到锁信息是存在对象的 mark work 中的。当对象状态为:

  • 偏向锁Biased),Mark Word存储的是偏向的线程ID;
  • 轻量级锁Lightweight Locked),Mark Word存储的是指向线程栈中 lock_record的指针;
  • 重量级锁Heavyweight Locked),存储的是指向堆中的 monitor对象指针。

重量级锁

状态为重量级锁(Heavyweight Locked) 时存储的是指向堆中的monitor 对象的指针 。那么这个monitor 对象包括哪些信息呢?

一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。其中cxq ,EntryList ,WaitSet都是ObjectWaiter的链表结构,owner指向持有锁的线程。

1、ContentionList :竞争队列,所有请求锁的线程首先被放在这个竞争队列中 2、EntryList:ContentionList中那些有资格成为候选资源的线程被移动到EntryList中 3、WaitSet:那些调用wait方法被阻塞的线程被放置在这里 4、OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程成功OnDeck 5、Owner:当前已经获取到资源的线程称为Owner 6、!Owner:当前释放锁的线程。

JVM 每次从队列尾部取出一个数据用于锁竞争候选者(OnDeck),但是在并发情况下,ContentionList 会被大量的并发线程进行CAS访问,为了降低尾部元素的竞争,JVM会将一部分线程移动到 EntryList 中作为候选竞争线程。Owner线程并不是直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁,这样虽然牺牲了一些公平性,但是能极大提高系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。

OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList 中,如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻被notify或者notifyAll唤醒,会重新进入EntryList 中。

处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的。

Synchronized是非公平锁,在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

偏向锁

偏向锁,顾名思义,它会偏向第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程竞争的情况,则线程是不需要触发同步机制,在这种情况下,就会给线程加一个偏向锁。

如果在运行过程中,遇到了其它线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁!

加锁过程

1、访问Mark workd中的偏向锁的标识是否设置为1,锁标志位是否为01,确认为可偏向状态。 2、如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤三。 3、如果线程ID并未指向当前线程,则通过CAS操作竞争锁,如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,则执行4. 4、如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致STW,但时间很短). 5、执行同步代码。

解锁过程

偏向锁的撤销在上述第四步中有提到。偏向锁只有遇到其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤离需要等待全局安全点(在某个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为01)或轻量级锁(标志位为00)的状态。

另外,偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0来关闭延迟。

轻量级锁

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入到锁争用的时候,偏向锁就会升级为你轻量级锁;

加锁过程

1.在代码进入同步块的时候,如果同步对象锁状态为无锁状态,或者偏向锁状态,虚拟机首先将当前线程的栈帧中建立一个锁记录(Lock Record)空间,用于存储锁对象目前的mark word的拷贝。 2、拷贝对象头中的mark word 复制到锁记录(Lock Record)中; 3.拷贝成功后,直接通过CAS指令Lock Record地址存储在对象头的mark word,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。如果失败,进入到步骤3。 4.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分(Displaced Mark Word)为null,起到了一个重入计数器的作用。然后结束。 5.走到这一步说明发生了竞争,需要膨胀为重量级锁,锁标志位变为10,

解锁过程

1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。 2.如果Lock RecordDisplaced Mark Word为null,代表这是一次重入,将obj设置为null后continue。 3.如果Lock RecordDisplaced Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为Displaced Mark Word。如果成功,则continue,否则膨胀为重量级锁。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Synchronized简介
  • 锁的几种形态
  • 重量级锁
  • 偏向锁
  • 轻量级锁
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档