乐趣区

关于java:同步锁synchronized追本溯源

1 同步锁 synchronized 寻根究底

引言
提到 synchronized,无论是在开发过程中和面试过程中经常遇到的问题
synchronized;也算是重灾区了

为什么说是重灾区?因为他不像其余的代码,是有源码, 能够查看的
synchronized 是一个关键字。间接是找不到源代码的

接下来
咱们会通过 java 内存指令码和 c ++ 源码(HotSpot 虚拟机源码)
给大家分析一下 synchronized 到底是怎么实现锁同步的

1.1 synchronized 场景回顾

指标:

synchronized 回顾

概念

synchronized:是 Java 中的关键字,是一种同步锁。

syn 属于哪种锁分类:

  • 乐观锁、乐观锁(syn)
  • 独享锁(syn)、共享锁
  • 偏心锁、非偏心锁(syn)
  • 互斥锁(syn)、读写锁
  • 可重入锁(syn)

tips:

synchronized JDK1.6 锁降级:无锁 -> 偏差锁 (非锁)-> 轻量级锁 -> 重量级锁(1.6 前都是)

多线程个性回顾(面试常问)

原子性:指一个操作或者多个操作,要么全副执行并且执行的过程不会被任何因素打断,要么就都不执行

可见性:是指多个线程拜访一个资源时,该资源的状态、值信息等对于其余线程都是可见的。

有序性:指程序中代码的执行程序(编译器会重排)

sync 能够残缺实现以上三个个性来保障线程安全性,cas 就无奈达到原子性。

这是什么原理呢?

1.2 反汇编寻找锁实现原理

指标

通过 javap 反汇编看一下 synchronized 到底是怎么加锁的

com.syn.BTest

public class BTest {private static Object object = new Object();

     public synchronized void testMethod() {System.out.println("Hello World -synchronized method");
    }

    public static void main(String[] args) {synchronized (object) {System.out.println("Hello World -synchronized block");
            
        }
    }
}

反汇编后,咱们将 到什么?

JDK 自带的一个工具:javap,对字节码进行反汇编:

//com.syn.BTest 
javap -v -c BTest.class

-v: 输入附加信息

-c: 对代码进行反汇编

反汇编后

解释
被 synchronized 润饰的代码块,多了两个指令
monitorenter、monitorexit
即 JVM 应用 monitorenter 和 monitorexit 两个指令实现同步

解释

办法调用时会查看办法的 ACC_SYNCHRONIZED 拜访标记是否被设置,如果设置了,执行线程将先获取 monitor,获取胜利之后能力执行办法体,办法执行完后再开释 monitor。也就是 jvm 会隐式调用 monitorenter 和
monitorexit。

  • monitorenter 原理

monitorenter 首先咱们来看一下 JVM 标准中对于 monitorenter 的形容

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
monitorenter:Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

monitorexit:The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

翻译如下:

  • monitorenter

每一个对象都会和一个监视器 monitor 关联。

监视器被占用时会被锁住,其余线程无奈来获取该 monitor。

当 JVM 执行某个线程的某个办法外部的 monitorenter 时,它会尝试去获取以后对象对应
的 monitor 的所有权。其过程如下:

  1. 若 monior 的进入数为 0,线程能够进入 monitor,并将 monitor 的进入数置为 1。以后线程成为
    monitor 的 owner(所有者)
  2. 若线程已领有 monitor 的所有权,容许它重入 monitor,则进入 monitor 的进入数加 1
  3. 若其余线程曾经占有 monitor 的所有权,那么以后尝试获取 monitor 的所有权的线程会被阻塞,直
    到 monitor 的进入数变为 0,能力从新尝试获取 monitor 的所有权。
  • monitorexit
  1. 能执行 monitorexit 指令的线程肯定是领有以后对象的 monitor 的所有权的线程。
  2. 执行 monitorexit 时会将 monitor 的进入数减 1。当 monitor 的进入数减为 0 时,以后线程退出
    monitor,不再领有 monitor 的所有权,此时其余被这个 monitor 阻塞的线程能够尝试去获取这个
    monitor 的所有权
    monitorexit 开释锁。
    monitorexit 插入在办法完结处和异样处,JVM 保障每个 monitorenter 必须有对应的 monitorexit。

tips(重要)

简略的了解,monitor 就是 jvm 底层的 c ++ 代码中的一个对象 ObjectMonitor。

这个对象里有个计数器,来记录以后对象锁有没有人用,用了多少次。

以及一些队列,寄存调度一些须要这把锁的线程。

对于 monitor 在 c ++ 里的构造,咱们下文再具体说。

总结:

1、synchronized 是靠 ObjectMonitor 来管制锁的

2、须要这把锁的线程在 monitor 的队列里被各种安顿

3、拿到锁的线程被 monitor 标记,计数加加,开释锁,须要将计数器减减操作

1.3 Monitor 详解

指标:Monitor 的地位

接下来咱们看它的具体内部结构,以及如何运作的。

1.3.1 Monitor 是什么

指标:通过 JVM 虚拟机源码剖析 synchronized 监视器 Monitor 到底是什么

tips:

c++ 源码理解即可,原理要明确

面试时很重要,面试过来了就不重要!(瞎说什么大瞎话)

在 HotSpot 虚拟机中,monitor 监视器是由 ObjectMonitor 实现的。

结构器代码 src/share/vm/runtime/objectMonitor.hpp

hpp 能够 include 蕴含 cpp 的货色,两者都是 c ++ 的代码

// 结构器
ObjectMonitor() {
  _header = NULL;
  _count = 0; 
  _waiters = 0,_recursions = 0; // 线程的重入次数
  _object = NULL; 
  _owner = NULL; // 以后线程,拿到锁的那位
  _WaitSet = NULL; // 期待队列,调 wait 的线程在这里
  _WaitSetLock = 0 ;
  _Responsible = NULL;
  _succ = NULL;
  _cxq = NULL; // 竞争队列,挣不到锁先进这里(可自旋)
  FreeNext = NULL;
  _EntryList = NULL; // 阻塞队列,来自 cxq(调 unlock 时)或者 waitSet(调 notify 时)_SpinFreq = 0;
  _SpinClock = 0;
  OwnerIsThread = 0;
}

留心这三个列表:

1)cxq(竞争列表)

cxq 是一个单向链表。被挂起线程期待从新竞争锁的链表, monitor 通过 CAS 将包装成 ObjectWaiter 写入到列表的头部。为了防止插入和取出元素的竞争,所以 Owner 会从列表尾部取元素。所以这个货色能够了解为一上来竞争没拿到锁的在这里长期待一会(1 级缓存)。

2)EntryList(锁候选者列表)

EntryList 是一个双向链表。当 EntryList 为空,cxq 不为空,Owener 会在 unlock 时,将 cxq 中的数据挪动到 EntryList。并指定 EntryList 列表头的第一个线程为 OnDeck 线程,其余线程就待在外面。所以这个货色能够认为是二次竞争锁还没拿到的(外面有一个马上就会拿到)。(2 级缓存)

备注:EntryList 跟 cxq 的区别

在 cxq 中的队列能够持续自旋期待锁,若达到自旋的阈值仍未获取到锁则会调用 park 办法挂起。而 EntryList 中的线程都是被挂起的线程。

3)WaitList

WatiList 是 Owner 线程地调用 wait()办法后进入的线程。进入 WaitList 中的线程在 notify()/notifyAll()调用后会被退出到 EntryList。

过程总结:

  • 期待锁的线程会待在_cxq 和 entry set 队列中,具体哪个和以后线程取锁的状况无关
  • entry set 的表头线程获取到对象的 monitor 后进入_Owner 区域并把 monitor 中的 _owner 变量设置为本人,同时 monitor 中的计数器 _count 加 1
  • 若线程调用 wait() 办法,将开释以后持有的 monitor,_owner变量复原为 null,_count自减 1,同时该线程进入 _WaitSet 汇合中期待被唤醒。
  • 若以后线程执行结束也将开释 monitor(锁)并复位变量的值,以便其余线程进入获取 monitor(锁)。

1.3.2 具体流程图(理解)

monitorenter

monitorenter 指令执行地位:

JVM 源码:src/share/vm/interpreter/interpreterRuntime.cpp

JVM 函数入口:InterpreterRuntime::monitorenter

最终调用:src/share/vm/runtime/objectMonitor.cpp 中的 ObjectMonitor::enter

monitorexit

执行 monitorexit 指令地位:

代码文件:src/share/vm/runtime/objectMonitor.cpp

调用函数:ObjectMonitor::exit

本文由传智教育博学谷 – 狂野架构师教研团队公布,转载请注明出处!

如果本文对您有帮忙,欢送关注和点赞;如果您有任何倡议也可留言评论或私信,您的反对是我保持创作的能源

退出移动版