首页 > Synchronized底层
头像
mlpan
编辑于 2021-05-07 10:28
+ 关注

Synchronized底层

Synchronized底层如何实现

相关的CSDN博客:

Synchronized关键字

synchronized俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能够持有对象锁,其他线程再想获取这个对象锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切
换所打断

Synchronized有三种使用方法:

  1. 修饰实例方法,作用相当于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
  2. 修饰静态方法,相当于在当前类对象加锁,进入同步代码前要获得当前类对象实例的锁。
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库的时候要获得给定对象的锁。
       image-20210328171234158

synchronized关键字的实现

synchronized不论是修饰代码块还是修饰方法都是通过持有对象锁来实现同步的。而这个对象的markword就指向了一个Monitor(锁/监视器)

1、java对象头的markword结构:

对象大致可以分为三个部分,分别是对象头,实例变量和填充字节,对象头分成两个部分:mark word和 klass word

图片说明

锁的类型和状态在对象头Mark Word中都有记录。在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。对于重量级锁对象的markword包含两个部分:指向重量级锁的指针和标志位

由此看来,monitor锁对象地址存在于每个Java对象的对象头中

2、Monitor结构:

每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

3、synchronized底层原理 = java对象头markword + 操作系统对象monitor:

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
图片说明

  1. ​ synchronized无论是加在同步代码块还是方法上,效果都是加在对象上,其原理都是对一个对象上锁
  2. ​ 如何给这个obj上锁呢?当一个线程Thread-1要执行临界区的代码时,首先会通过obj对象的markword指向一个monitor锁对象
  3. ​ 当Thread-1线程持有monitor对象后,就会把monitor中的owner变量设置为当前线程Thread-1,同时计数器count+1表示当前对象锁被一个线程获取。
  4. ​ 当另一个线程Thread-2想要执行临界区的代码时,要判断monitor对象的属性Owner是否为null,如果为null,Thread-2线程就持有了对象锁可以执行临界区的代码,如果不为null,Thread-2线程就会放入monitor的EntryList阻塞队列中,处于阻塞状态Blocked。
  5. ​ 当Thread-0将临界区的代码执行完毕,将释放monitor(锁)并将owner变量置为null,同时计算器count-1,并通知EntryList阻塞队列中的线程,唤醒里面的线程

Synchronized 上锁原理

实现原理: JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。

具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。

对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁。

流程图如下:

image-20210328171358568

monitorenter 指令:

​ 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

​ 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者

​ 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.(可重入锁的原因)

​ 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit指令:

​ 执行monitorexit的线程必须是持有obj锁对象的线程

​ 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程释放monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

​ Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出IllegalMonitorStateException的异常的原因。

使用javap -c Synchronize可以查看编译之后的具体信息。

image-20210328171429805

可以看到在同步块的入口和出口分别有monitorentermonitorexit指令。当执行monitorenter指令时,线程试图获取锁也就是获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

全部评论

(3) 回帖
加载中...
话题 回帖

推荐话题

相关热帖

热门推荐