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

本文由传智教育博学谷 – 狂野架构师教研团队发布
如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力
转载请注明出处!