java八股文面试[多线程]——Synchronized的底层实现原理

笔试:画出Synchronized 线程状态流转实现原理图

java八股文面试[多线程]——Synchronized的底层实现原理_第1张图片

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 翻译为中文的意思是同步,也称之为”同步锁“。

synchronized的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。

synchronized关键字可以实现什么类型的锁?
  悲观锁:synchronized关键字实现的是悲观锁,每次访问共享资源时都会上锁。
  非公平锁:synchronized关键字实现的是非公平锁,即线程获取锁的顺序并不一定是按照线程阻塞的顺序。
  可重入锁:synchronized关键字实现的是可重入锁,即已经获取锁的线程可以再次获取锁。
  独占锁或者排他锁:synchronized关键字实现的是独占锁,即该锁只能被一个线程所持有,其他线程均被阻塞。

Synchronized的使用方式
主要有3种使用方式:
java八股文面试[多线程]——Synchronized的底层实现原理_第2张图片

1.修饰实例方法:作用于当前实例加锁
public synchronized void method(){
// 代码
}

2.修饰静态方法:作用于当前类对象加锁
public static synchronized void method(){
// 代码
}

3.修饰代码块:指定加锁对象,对给定对象加锁
synchronized(this){
//代码
}

java八股文面试[多线程]——Synchronized的底层实现原理_第3张图片

Synchronized的底层实现
synchronized的底层实现是完全依赖JVM虚拟机的,所以谈synchronized的底层实现,就不得不谈数据在JVM内存的存储:Java对象头,以及Monitor对象监视器。

1.Java对象头
在JVM虚拟机中,对象在内存中的存储布局,可以分为三个区域:
对象头(Header)
实例数据(Instance Data)
对齐填充(Padding)
Java对象头主要包括两部分数据:

java八股文面试[多线程]——Synchronized的底层实现原理_第4张图片

1)类型指针(Klass Pointer)

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

2)标记字段(Mark Word)

用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄锁状态标志、线程持有的锁偏向线程 ID、偏向时间戳等等,它是实现轻量级锁偏向锁的关键.

所以,很明显synchronized使用的锁对象是存储在Java对象头里的标记字段里。

2.Monitor

monitor描述为对象监视器,可以类比为一个特殊的房间,这个房间中有一些被保护的数据,monitor保证每次只能有一个线程能进入这个房间进行访问被保护的数据,进入房间即为持有monitor,退出房间即为释放monitor。

使用syncrhoized加锁的同步代码块在字节码引擎中执行时,主要就是通过锁对象的monitor的取用(monitorenter)与释放(monitorexit)来实现的。

首先来看在方法上上锁,我们就新定义一个同步方法然后进行反编译,查看其字节码:

java八股文面试[多线程]——Synchronized的底层实现原理_第5张图片

java八股文面试[多线程]——Synchronized的底层实现原理_第6张图片

可以看到在add方法的flags里面多了一个ACC_SYNCHRONIZED标志,这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,知道该锁被释放。

从反编译的同步代码块可以看到同步块是由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。当获取锁失败时会被阻塞,一直等待锁被释放。

但是为什么会有两个monitorexit呢?其实第二个monitorexit是来处理异常的,仔细看反编译的字节码,正常情况下第一个monitorexit之后会执行goto指令,而该指令转向的就是23行的return,也就是说正常情况下只会执行第一个monitorexit释放锁,然后返回。而如果在执行中发生了异常,第二个monitorexit就起作用了,它是由编译器自动生成的,在发生异常时处理异常然后释放掉锁。

3.线程状态流转在Monitor上体现

当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:

Contention List:所有请求锁的线程将被首先放置到该竞争队列
Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程称为Owner
!Owner:释放锁的线程

java八股文面试[多线程]——Synchronized的底层实现原理_第7张图片

每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  //锁的计数器,获取锁时count数值加1,释放锁时count值减1
    _waiters      = 0,  //等待线程数
    _recursions   = 0;  // 线程重入次数
    _object       = NULL;  // 存储Monitor对象
    _owner        = NULL;  // 持有当前线程的owner
    _WaitSet      = NULL;  // wait状态的线程列表
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;  // 阻塞在EntryList上的单向线程列表
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于等待锁状态block状态的线程列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

其中 _owner、_WaitSet和_EntryList 字段比较重要,它们之间的转换关系如下图  

java八股文面试[多线程]——Synchronized的底层实现原理_第8张图片

ObjectMonitor中有两个队列_WaitSet_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入_EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析)

知识来源:

Synchronized的底层实现原理(看这篇就够了)_synchronized底层实现原理_mikechen的互联网架构的博客-CSDN博客

https://www.cnblogs.com/wffzk/p/16639472.html

深入理解synchronized底层原理,一篇文章就够了! - 知乎

你可能感兴趣的:(java八股文,java,面试,开发语言)