首页 > 其他 > 详细

锁:synchronized原理

时间:2020-08-22 18:24:17      阅读:70      评论:0      收藏:0      [点我收藏+]

1、反汇编方式理解synchronized原理

(1)源码

public class Test {
    private static Object obj = new Object();

    public static void main(String[] args) {
        synchronized (obj) {
            System.out.println("1");
        }
    }

    public synchronized void test() {
        System.out.println("a");
    }
}

(2)反汇编查看字节码指令

技术分享图片

 在monitorenter和monitorexit之间执行的是代码逻辑

技术分享图片

(3)monitorenter

  每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:

  • 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
  • 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
  • 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

  synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量:owner拥有这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待

(4)monitorexit

  能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。

执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个


2、查看JVM源码

(1)下载源代码(因为synchronized的源代码是c++写的)

技术分享图片

 

 

 选择版本:

技术分享图片

 

 

 技术分享图片

 

 

 选择格式:

技术分享图片

 

 

 (2)查看源代码

在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor主要数据结构如下:

ObjectMonitor() {
  _header    = NULL;
  _count     = 0;
  _waiters    = 0,
  _recursions  = 0;  // 线程的重入次数
_object    = NULL; // 存储该monitor的对象
  _owner     = NULL; // 标识拥有该monitor的线程
  _WaitSet    = NULL; // 处于wait状态的线程,会被加入到_WaitSet
  _WaitSetLock  = 0 ;
  _Responsible  = NULL;
  _succ     = NULL;
  _cxq      = NULL; // 多线程竞争锁时的单向列表
  FreeNext    = NULL;
  _EntryList   = NULL; // 处于等待锁block状态的线程,会被加入到该列表
  _SpinFreq   = 0;
  _SpinClock   = 0;
  OwnerIsThread = 0;
}

技术分享图片

 

 

  •  _owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
  •  _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
  • _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中。
  • _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。

 

3、monitor竞争

执行monitorenter时,会调用InterpreterRuntime.cpp(位于:src/share/vm/interpreter/interpreterRuntime.cpp) 的 InterpreterRuntime::monitorenter函数。具体代码可参见HotSpot源码

if (UseBiasedLocking) {//是否用偏向锁
  // Retry fast entry if bias is revoked to avoid unnecessary inflation
  ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
  ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
 assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
    "must be NULL or an object");

对于重量级锁,monitorenter函数中会调用 ObjectSynchronizer::slow_enter最终调用 ObjectMonitor::enter(位于:src/share/vm/runtime/objectMonitor.cpp),源码如下:

void ATTR ObjectMonitor::enter(TRAPS) {
 // The following code is ordered to check the most common cases first
 // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
 Thread * const Self = THREAD ;
 void * cur ;
 // 通过CAS操作尝试把monitor的_owner字段设置为当前线程
 cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
 if (cur == NULL) {
  // Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
  assert (_recursions == 0  , "invariant") ;
  assert (_owner    == Self, "invariant") ;
  // CONSIDER: set or assert OwnerIsThread == 1
  return ;
}
 // 线程重入,recursions++
 if (cur == Self) {
  // TODO-FIXME: check for integer overflow! BUGID 6557169.
  _recursions ++ ;
  return ;
}
 // 如果当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程
if (Self->is_lock_owned ((address)cur)) {
  assert (_recursions == 0"internal state error");
  _recursions = 1 ;
  // Commute owner from a thread-specific on-stack BasicLockObject address to
  // a full-fledged "Thread *".
  _owner = Self ;
  OwnerIsThread = 1 ;
  return ;
}
 // 省略一些代码
 for (;;) {
  jt->set_suspend_equivalent();
  // cleared by handle_special_suspend_equivalent_condition()
  // or java_suspend_self()
 
// 如果获取锁失败,则等待锁的释放;
  EnterI (THREAD) ;
  if (!ExitSuspendEquivalent(jt)) break ;
  //
  // We have acquired the contended monitor, but while we were
  // waiting another thread suspended us. We don‘t want to enter
  // the monitor while suspended because that would surprise the
  // thread that suspended us.
  //
    _recursions = 0 ;
  _succ = NULL ;
  exit (false, Self) ;
  jt->java_suspend_self();
}
 Self->set_current_pending_monitor(NULL);
}
  • 通过CAS尝试把monitor的owner字段设置为当前线程。
  • 如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions ++ ,记录重入的次数。
  • 如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回。
  • 如果获取锁失败,则等待锁的释放

 

4、monitor等待

竞争失败等待调用的是ObjectMonitor对象的EnterI方法(位于:src/share/vm/runtime/objectMonitor.cpp),源码如下所示:

void ATTR ObjectMonitor::EnterI (TRAPS) {
  Thread * Self = THREAD ;
// Try the lock - TATAS
  if (TryLock (Self) > 0) {
    assert (_succ != Self       , "invariant") ;
    assert (_owner == Self       , "invariant") ;
    assert (_Responsible != Self    , "invariant") ;
    return ;
 }
 
  if (TrySpin (Self) > 0) {
    assert (_owner == Self    , "invariant") ;
    assert (_succ != Self     , "invariant") ;
    assert (_Responsible != Self , "invariant") ;
    return ;
 }//以上代码是在没有获得锁的情况下再次尝试获取锁
 
// 省略部分代码
  // 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ;
  ObjectWaiter node(Self) ;
  Self->_ParkEvent->reset() ;
  node._prev  = (ObjectWaiter *) 0xBAD ;
  node.TState  = ObjectWaiter::TS_CXQ ;
// 通过CAS把node节点push到_cxq列表中
  ObjectWaiter * nxt ;
  for (;;) {
    node._next = nxt = _cxq ;
    if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
    // Interference - the CAS failed because _cxq changed. Just retry.
    // As an optional optimization we retry the lock.
    if (TryLock (Self) > 0) {
      assert (_succ != Self     , "invariant") ;
      assert (_owner == Self     , "invariant") ;
      assert (_Responsible != Self  , "invariant") ;
      return ;
   }
 }
  // 省略部分代码
  for (;;) {
// 线程在被挂起前做一下挣扎,看能不能获取到锁
    if (TryLock (Self) > 0) break ;
    assert (_owner != Self, "invariant") ;
    if ((SyncFlags & 2) && _Responsible == NULL) {
     Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
   }
    // park self
    if (_Responsible == Self || (SyncFlags & 1)) {
      TEVENT (Inflated enter - park TIMED) ;
      Self->_ParkEvent->park ((jlong) RecheckInterval) ;
      // Increase the RecheckInterval, but clamp the value.
      RecheckInterval *= 8 ;
      if (RecheckInterval > 1000) RecheckInterval = 1000 ;
   } else {
      TEVENT (Inflated enter - park UNTIMED) ;
 // 通过park将当前线程挂起(由于用户和系统的需要,例如,终端用户需要暂停程序研究其执行情况或对其进行修改、OS为了提
高内存利用率需要将暂时不能运行的进程(处于就绪或阻塞队列的进程)调出到磁盘),等待被唤醒
      Self->_ParkEvent->park() ;    }     if (TryLock(Self) > 0) break ;     // 省略部分代码  }   // 省略部分代码 }

当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁,TryLock方法实现如下:

int ObjectMonitor::TryLock (Thread * Self) {
 for (;;) {
   void * own = _owner ;
   if (own != NULL) return 0 ;
   if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
    // Either guarantee _recursions == 0 or set _recursions = 0.
    assert (_recursions == 0"invariant") ;
    assert (_owner == Self, "invariant") ;
    // CONSIDER: set or assert that OwnerIsThread == 1
    return 1 ;
  }
   // The lock had been free momentarily, but we lost the race to the lock.
   // Interference -- the CAS failed.
   // We can either return -1 or retry.
   // Retry doesn‘t make as much sense because the lock was just acquired.
   if (true) return -1 ;
 }
}
  • 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ。
  • 在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中。
  • node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒。
  • 当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁。

 

5、monitor释放

  • 当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其它线程机会执行同步代码,在HotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于ObjectMonitor的exit方法中。(位于:src/share/vm/runtime/objectMonitor.cpp)
  • 退出同步代码块时会让_recursions减1,当_recursions的值减为0时,说明线程释放了锁。
  • 根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作最终由unpark完成
  • 被唤醒的线程,会回到 void ATTR ObjectMonitor::EnterI (TRAPS) 的第600行,继续执行monitor的竞争。

 

6、monitor是重量级锁

  可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语言中是一个重量级(Heavyweight)的操作。

内核:可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。
用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。
系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);但是当它调用系统调用执行某些操作时,例如 I/O调用,此时需要陷入内核中运行,我们就称进程处于内核运行态(或简称为内核态)。 系统调用的过程可以简单理解为:

  • 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈, 以此表明需要操作系统提供的服务。
  • 用户态程序执行系统调用。
  • CPU切换到内核态,并跳到位于内存指定位置的指令。
  • 系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。
  • 系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。

  由此可见用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在synchronized未优化之前,效率低的原因。







 



 



 

 

 

 

 

 

锁:synchronized原理

原文:https://www.cnblogs.com/zhai1997/p/13544523.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!