1. 简介

synchronized 关键字主要解决多个线程之间访问资源的同步性,被该关键词修饰的方法或者代码块,在某一时刻只有一个线程能够访问。
synchronized 实现同步主要有以下三种方式:

  • 修饰普通方法,锁的是当前实例对象。
  • 修饰静态方法,锁的是当前类的Class对象。
  • 修饰代码块,锁的是synchronized括号里配置的对象。

2. 实现

2.1 synchronized 修饰普通方法

代码及其反编译结果如下所示。我们可以看到,使用synchronized修饰普通方法,会给该方法加上 ACC_SYNCHRONIZED 标识,标识该方法时一个同步方法。

  1. public synchronized void normalMethod(){
  2. System.out.println("normalMethod");
  3. }

image.png

2.2 synchronized 修饰静态方法

代码及其反编译结果如下所示。我们可以看到,静态方法反编译结果与普通方法类似,都是加上了ACC_SYNCHRONIZED标识。

  1. public synchronized static void staticMethod(){
  2. System.out.println("staticMethod");
  3. }

image.png

2.3 synchronized 修饰代码块

代码及其反编译结果如下所示。我们可以看到,使用synchronized修饰代码块,会在代码块前后分别加上monitorenter和monirotexit指令,通过这两个指令实现同步。

  1. public void synchronizedThis() {
  2. synchronized (this) {
  3. System.out.println("synchronized this");
  4. }
  5. }

image.png

2.4 monitor 和 ACC_SYNCHRONIZED

Java虚拟机中的同步时用monitor的进入和退出来实现。无论显式同步(有明确的monitorenter 和 monitorexit),还是隐式同步(依赖方法调用和返回指令实现),都是如此。 —Java虚拟机规范8

通过上文,我们可知同步代码块是通过monitorenter和monitorexit来实现同步的,那么ACC_SYNCHRONIZED又是如何实现的呢?

虚拟机可以根据ACC_SYNCHRONIZED标识判断区分一个方法是否是同步方法。当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置,如果设置了,执行线程将进入监视器,然后执行方法,最后在方法完成(无论正常完成还是非正常完成)时退出监视器。 —Java虚拟机规范8

综上,synchronized修饰代码块和修饰方法,最终都是通过进入和退出监视器来进行同步的。

2.5 monitor 实现

先来看一个计算机操作系统的概念—管程,以下是维基百科该词条的定义。

管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。

在Jvm中,管程是使用c++通过ObjectMonitor对象实现的,其数据结构如下所示:

  1. ObjectMonitor() {
  2. _header = NULL;//markOop对象头
  3. _count = 0;
  4. _waiters = 0,//等待线程数
  5. _recursions = 0;//重入次数
  6. _object = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
  7. _owner = NULL;//指向获得ObjectMonitor对象的线程或基础锁
  8. _WaitSet = NULL;//处于wait状态的线程,会被加入到wait set;
  9. _WaitSetLock = 0 ;
  10. _Responsible = NULL ;
  11. _succ = NULL ;
  12. _cxq = NULL ;
  13. FreeNext = NULL ;
  14. _EntryList = NULL ;//处于等待锁block状态的线程,会被加入到entry set;
  15. _SpinFreq = 0 ;
  16. _SpinClock = 0 ;
  17. OwnerIsThread = 0 ;
  18. _previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
  19. }

_cxq(竞争列表)
cxq是一个单向链表,线程被挂起之后会被包装成ObjectWaiter写入到链表的头部。为了避免插入和取出元素的竞争,所以Owner会从列表尾部取元素。

_EntryList(锁候选者列表)
EntryList是一个双向链表。当EntryList为空,cxq不为空,Owener会在unlock时,将cxq中的数据移动到EntryList。并指定EntryList列表头的第一个线程为OnDeck线程。EntryList跟cxq的区别,在cxq中的队列可以继续自旋等待锁,若达到自旋的阈值仍未获取到锁则会调用park方法挂起;而EntryList中的线程都是被挂起的线程。

_WaitList

WatiList是Owner线程调用wait()方法后进入的区域。进入WaitList中的线程在notify()/notifyAll()调用后会被加入到EntryList。

_Owner
当前锁持有者。

_OnDeckThread
可进行锁竞争的线程。若一个线程被设置为OnDeck,则表明其可以进行tryLock操作,若获取锁成功,则变为Owner,否则仍将其回插到EntryList头部。OnDeckThread竞争锁失败的原因,cxq中的线程可以进行自旋竞争锁,所以OnDeckThread有可能竞争失败。

3. 锁优化

JDK1.6 对Synchronized的实现做了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁优化,通过这些优化,可以大大地减少锁操作的开销。

优化之后的锁一共有四种状态,分别是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态。
先来了解一个概念,Mark Word。HotSpot虚拟机的对象头分为两个部分,第一部分是先来了解一个概念,Mark Word。HotSpot虚拟机的对象头分为两个部分,第一部分是;第二部分是类型指针,此处不予关注。在32位的HotSpot虚拟机中,Mark Word的长度为32个比特,其中25个比特用于存储HashCode,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,1个比特固定为0。不同锁状态下,HotSpot虚拟机中对象头Mark Word存储的内容如下表所示。

HotSpot虚拟机对象头Mark Word
锁状态 32bit
25bit 4bit 1bit 2bit
23bit 2bit 偏向模式 标志位
未锁定 对象哈希码 分代年龄 0 01
轻量级锁定 指向调用栈中锁记录的指针 00
重量级锁的 指向重量级锁的指针 10
GC标记 11
可偏向 线程ID Epoch 分代年龄 1 01

3.1 偏向锁

在大多数情况下,锁不仅不存在竞争,而且总是由同一个线程多次获得,为了让线程获取锁的代价更低从而引入了偏向锁。当锁对象第一次被线程获取时,会将对象头的锁标志设置为01,并将偏向模式设置为1,同时使用CAS操作把当前线程ID记录在对象的Mark Word中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,都不用再进行加锁和解锁操作。

偏向锁的撤销:偏向锁只有在遇到其他线程尝试竞争偏向锁时,才会进行释放。偏向锁的撤销,需要等待全局安全点,然后判断偏向锁的线程是否存活且还在同步代码块中,如果还在,则将偏向锁升级成轻量级锁,否则直接撤销偏向锁,变为无锁状态,之后不再使用偏向模式。

ps:当JVM启用了偏向锁模式(1.6以上默认开启),当新创建一个对象的时候,如果该对象所属的class没有关闭偏向锁模式(默认所有class的偏向模式都是是开启的),那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。

HashCode问题:当对象进入偏向状态的时候,Mark Word大部分空间都用于存储线程id了,那么原来的HashCode怎么办?

对于未重写hashCode方法的对象来说,hashCode来源于Object::hashCode()方法,返回的是对象的一致性hashCode,这个值是强制保证不变的,它通过在Mark Word中存储计算结果来保证该方法取到的值永远不会发生改变。因此,当一个对象计算过一致性hashCode之后,它就再也无法进入偏向锁状态;当一个对象当前正处于偏向锁状态,又收到计算其一致性hashCode请求时,它的偏向锁状态会被立即撤销,并且锁会膨胀为重量级锁。而在重量级锁中,对象头指向重量级锁的位置(ObjectMonitor对象的起始位置),ObjectMonitor对象里有字段可以记录非加锁状态下的Mark Word,其中包括原来的hashCode。

3.2 轻量级锁

轻量级锁设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

获取过程

  1. 在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为01),虚拟机首先在当前线程额栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前Mark Word的拷贝(Displaced Mard Word)。如下图所示:

image.png

  1. 将对象头的Mark Word拷贝到锁记录中。
  2. 虚拟机将使用CAS操作将对象头的Mark Word更新为执行锁记录的指针,并将锁记录里的owner指针指向对象头的Mark Word。如下图所示

image.png

  1. 如果第3步的CAS操作成功了,则该线程获取该对象的锁,将锁对象的Mark Word的锁标志位设置为00(轻量级锁)。
  2. 如果第3步的CAS操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是则说明是重入,那么直接进入代码块执行;如果不是,说明锁对象被其他线程抢占了,此时则需要膨胀成重量级锁。

轻量级锁重入的情况:在第5步中,存在轻量级锁重入的可能,当发生这种情况是,虚拟机会在当前的栈帧中分配一个Lock Record(其Displaced Mard Word为null,owner还是指向对象头的Mark Word),如下图所示:
synchronized-轻量级锁重入.png

轻量级锁释放:使用CAS操作把锁对象的Mark Word和线程中复制的Displaced Mark Word进行替换,如果成功,则释放成功;如果失败,则说明当前有线程在竞争,则升级为重量级锁。

轻量级锁是否存在自旋:看到很多文章以及《Java并发编程的艺术》里描述了轻量级锁CAS失败会进行自旋等待,通过查看hotspot源码发现轻量级锁并没有进行自旋的操作。获取轻量级锁的入口代码如下所示,是对monitorenter指令的解析在(interpreterRuntime.cpp中):

  1. IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  2. // 省略无关代码
  3. Handle h_obj(thread, elem->obj());
  4. assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
  5. "must be NULL or an object");
  6. // 偏向锁 UseBiasedLocking表示虚拟机是否开启偏向锁
  7. if (UseBiasedLocking) {
  8. // Retry fast entry if bias is revoked to avoid unnecessary inflation
  9. ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  10. }
  11. // 轻量级锁
  12. else {
  13. ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  14. }

进入ObjectSynchronizer::slow_enter方法,该方法代码在synchronizer.cpp中,代码如下所示:
主要逻辑:

  1. 判断是否无锁状态
  2. 处于无锁状态,将Mark Word保存到BasicLock对象的_displaced_header字段中
  3. 通过cas将Mark Word 更新为指向BasicLock对象的指针
    1. 如果更新成功,表示获取轻量级锁成功,返回,执行同步代码
    2. 如果更新失败,跳到第4
  4. 如果更新失败,则判断当前是否为重入(处于加锁状态,且Mark Word的ptr指针指向当前线程的栈帧)
    1. 如果重入,返回,执行同步代码
    2. 如果不是,跳到第5
  5. 此时说明有多个线程竞争轻量级锁,需要膨胀为重量级锁 ```cpp void ObjectSynchronizer::slow_enter(Handle obj, BasicLock lock, TRAPS) { markOop mark = obj->mark(); // mark->is_neutral() 判断是不是无锁状态,该方法定义在markOop.hpp中 if (mark->is_neutral()) { //通过CAS将mark word更新为指向BasicLock对象的指针,更新成功表示获得了轻量级锁 lock->set_displaced_header(mark); if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } } else // 如果Mark Word处于加锁状态,且指向当前线程的栈帧,则为重入操作,直接返回 if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { assert(lock != mark->locker(), “must not re-lock the same lock”); assert(lock != (BasicLock)obj->mark(), “don’t relock with same BasicLock”); lock->set_displaced_header(NULL); return; }

if 0

if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) { lock->set_displaced_header (NULL) ; return ; }

endif

// 此时有多个线程竞争轻量级锁,调用inflate方法使轻量级锁需要膨胀为重量级锁

// 将Displaced Mark Word 设置为一个特殊值,代表该所正在用重量级锁 lock->set_displaced_header(markOopDesc::unused_mark()); // inflate方法将锁膨胀为重量级锁,该方法会返回一个ObjectMonitor对象,然后调用该对象的enter方法 ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD); }

  1. **由上述代码可知,轻量级锁,不存在自旋操作。**假如有两个线程同时进入上述代码的第5行(if (mark->is_neutral())),则会有一个线程cas成功,另一个线程失败,进入第5步将该锁膨胀为重量级锁。
  2. <a name="SNAo4"></a>
  3. ### 3.3 重量级锁
  4. **重量级锁膨胀过程**<br />上文提到,轻量级锁存在竞争时,会通过ObjectSynchronizer::inflate方法进行膨胀。ObjectSynchronizer::inflate方法会将锁膨胀为重量级锁,并且返回一个ObjectMonitor对象,该方法使用一个for循环处理多线程调用该方法的情况,在for循环内部针对锁对象当前的状态进行不同的处理:
  5. 1. 已经是重量级锁,直接返回
  6. 1. 膨胀中状态,进行忙等待,然后continue再次进入for循环
  7. 1. 轻量级锁状态,进行膨胀操作,如果cas失败则释放当前ObjectMonitor,然后continue再次进入for循环
  8. 1. 无锁状态,进行膨胀操作,如果cas失败则释放当前ObjectMonitor,然后continue再次进入for循环
  9. 具体代码如下所示:
  10. ```cpp
  11. ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
  12. EventJavaMonitorInflate event;
  13. // for循环,处理多个线程同时调用该方法的情况
  14. for (;;) {
  15. const markOop mark = object->mark() ;
  16. // * Inflated 重量级锁 - 直接返回
  17. // * Stack-locked 轻量级锁 - 膨胀
  18. // * INFLATING 膨胀中 - 忙等待直到膨胀完成
  19. // * Neutral 无锁 - 膨胀
  20. // * BIASED 偏向锁 - 非法状态,此处不会出现
  21. // CASE: inflated
  22. if (mark->has_monitor()) {
  23. ObjectMonitor * inf = mark->monitor() ;
  24. //...
  25. return inf ;
  26. }
  27. // CASE: inflation in progress
  28. // 膨胀中,进行自旋,ReadStableMark方法会进行spin/yield/park等操作
  29. if (mark == markOopDesc::INFLATING()) {
  30. TEVENT (Inflate: spin while INFLATING) ;
  31. ReadStableMark(object) ;
  32. continue ;
  33. }
  34. // CASE: stack-locked
  35. // 轻量级锁状态,分配一个ObjectMonitor对象,进行初始化。
  36. if (mark->has_locker()) {
  37. // 分配一个ObjectMonitor对象
  38. ObjectMonitor * m = omAlloc (Self) ;
  39. m->Recycle();
  40. m->_Responsible = NULL ;
  41. m->OwnerIsThread = 0 ;
  42. m->_recursions = 0 ;
  43. m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;
  44. // 将锁对象的Mark Word 设置为INFLATING(0)状态
  45. markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
  46. //CAS失败,说明冲突了,释放监视器锁,进行自旋等待(进入下一次循环)
  47. if (cmp != mark) {
  48. omRelease (Self, m, true) ;
  49. continue ;
  50. }
  51. markOop dmw = mark->displaced_mark_helper() ;
  52. assert (dmw->is_neutral(), "invariant") ;
  53. // CAS成功,设置字段:_header _owner _object
  54. m->set_owner(mark->locker());
  55. m->set_object(object);
  56. guarantee (object->mark() == markOopDesc::INFLATING(), "invariant") ;
  57. // 将锁对象头设置为重量级锁状态
  58. object->release_set_mark(markOopDesc::encode(m));
  59. if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
  60. TEVENT(Inflate: overwrite stacklock) ;
  61. if (TraceMonitorInflation) {
  62. if (object->is_instance()) {
  63. ResourceMark rm;
  64. tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
  65. (void *) object, (intptr_t) object->mark(),
  66. object->klass()->external_name());
  67. }
  68. }
  69. if (event.should_commit()) {
  70. post_monitor_inflate_event(&event, object);
  71. }
  72. return m ;
  73. }
  74. // 无锁状态,分配ObjectMonitor对象,并初始化
  75. // CASE: neutral
  76. assert (mark->is_neutral(), "invariant");
  77. ObjectMonitor * m = omAlloc (Self) ;
  78. // prepare m for installation - set monitor to initial state
  79. m->Recycle();
  80. m->set_header(mark);
  81. m->set_owner(NULL);
  82. m->set_object(object);
  83. m->OwnerIsThread = 1 ;
  84. m->_recursions = 0 ;
  85. m->_Responsible = NULL ;
  86. m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;
  87. // 用CAS替换对象头的Mark Word为重量级锁状态,不成功说明有其他线程在进行膨胀,此处需要释放当前的ObjectMonitor
  88. if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
  89. m->set_object (NULL) ;
  90. m->set_owner (NULL) ;
  91. m->OwnerIsThread = 0 ;
  92. m->Recycle() ;
  93. omRelease (Self, m, true) ;
  94. m = NULL ;
  95. continue ;
  96. }
  97. if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
  98. TEVENT(Inflate: overwrite neutral) ;
  99. if (TraceMonitorInflation) {
  100. if (object->is_instance()) {
  101. ResourceMark rm;
  102. tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
  103. (void *) object, (intptr_t) object->mark(),
  104. object->klass()->external_name());
  105. }
  106. }
  107. if (event.should_commit()) {
  108. post_monitor_inflate_event(&event, object);
  109. }
  110. return m ;
  111. }
  112. }


重量级锁获取过程**
在重量级锁膨胀完成之后,会调用inflate方法返回的ObjectMonitor的enter方法,进行monitor的竞争。该方法逻辑如下所示:

  1. 如果当前是无锁、重入、轻量级锁状态(当前线程持有),则进行简单操作并返回
  2. TrySpin方法自旋尝试获得锁
  3. 调用EnterI方法获得锁或者阻塞

    1. void ATTR ObjectMonitor::enter(TRAPS) {
    2. // 省略无关代码
    3. Thread * const Self = THREAD ;
    4. void * cur ;
    5. // cas把当前线程设置到_owner字段,返回cur=原先值。如果成功,说明原先_owner=NULL,是无锁状态,当前线程直接获取锁。
    6. cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
    7. if (cur == NULL) {
    8. return ;
    9. }
    10. // 重入,增加重入次数,返回
    11. if (cur == Self) {
    12. _recursions ++ ;
    13. return ;
    14. }
    15. // 当前线程是之前持有轻量级锁的线程,此时cur还是指向LockRecord的指针。在inflate方法进行膨胀时会把锁对象设置到_owner
    16. // 重入计数重置为1,设置_owner为当前线程
    17. if (Self->is_lock_owned ((address)cur)) {
    18. assert (_recursions == 0, "internal state error");
    19. _recursions = 1 ;
    20. _owner = Self ;
    21. OwnerIsThread = 1 ;
    22. return ;
    23. }
    24. // we forgo posting JVMTI events and firing DTRACE probes.
    25. // 在调用系统的同步操作之前,先尝试自旋获取锁,自旋的过程中获得锁则直接返回
    26. if (Knob_SpinEarly && TrySpin (Self) > 0) {
    27. Self->_Stalled = 0 ;
    28. return ;
    29. }
    30. for (;;) {
    31. // 调用EnterI进行同步操作
    32. EnterI (THREAD) ;
    33. // ...
    34. }
    35. Self->set_current_pending_monitor(NULL);
    36. }

    EnterI方法:该方法为真正获取重量级锁的方法。其主要代码如下所示,以下代码保留了主要步骤的代码,其他代码此处省略了。
    主要步骤如下所示:

  4. 将当前线程插入到_cxq的队首

  5. 挂起当前线程
  6. 唤醒后重新尝试获取锁

    1. void ATTR ObjectMonitor::EnterI (TRAPS) {
    2. Thread * Self = THREAD ;
    3. // Try the lock - TATAS
    4. // 尝试获取锁
    5. if (TryLock (Self) > 0) {
    6. assert (_succ != Self , "invariant") ;
    7. assert (_owner == Self , "invariant") ;
    8. assert (_Responsible != Self , "invariant") ;
    9. return ;
    10. }
    11. //...
    12. // 尝试自旋获取锁
    13. if (TrySpin (Self) > 0) {
    14. assert (_owner == Self , "invariant") ;
    15. assert (_succ != Self , "invariant") ;
    16. assert (_Responsible != Self , "invariant") ;
    17. return ;
    18. }
    19. ObjectWaiter node(Self) ;
    20. Self->_ParkEvent->reset() ;
    21. node._prev = (ObjectWaiter *) 0xBAD ;
    22. node.TState = ObjectWaiter::TS_CXQ ;
    23. // 将当前线程封装成ObjectWaiter对象,push到_cxq队列的对头,此时可能多个线程再进行,所以需要自旋cas
    24. ObjectWaiter * nxt ;
    25. for (;;) {
    26. node._next = nxt = _cxq ;
    27. if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
    28. // cas 失败,再尝试获取锁
    29. if (TryLock (Self) > 0) {
    30. return ;
    31. }
    32. }
    33. if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
    34. // Try to assume the role of responsible thread for the monitor.
    35. // CONSIDER: ST vs CAS vs { if (Responsible==null) Responsible=Self }
    36. Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
    37. }
    38. TEVENT (Inflated enter - Contention) ;
    39. int nWakeups = 0 ;
    40. int RecheckInterval = 1 ;
    41. // 自旋挂起
    42. for (;;) {
    43. // 在挂起之前尝试获取锁
    44. if (TryLock (Self) > 0) break ;
    45. assert (_owner != Self, "invariant") ;
    46. if ((SyncFlags & 2) && _Responsible == NULL) {
    47. Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
    48. }
    49. // park self
    50. // 将当前线程挂起
    51. if (_Responsible == Self || (SyncFlags & 1)) {
    52. TEVENT (Inflated enter - park TIMED) ;
    53. Self->_ParkEvent->park ((jlong) RecheckInterval) ;
    54. // Increase the RecheckInterval, but clamp the value.
    55. RecheckInterval *= 8 ;
    56. if (RecheckInterval > 1000) RecheckInterval = 1000 ;
    57. } else {
    58. TEVENT (Inflated enter - park UNTIMED) ;
    59. Self->_ParkEvent->park() ;
    60. }
    61. // 唤醒时,尝试获取锁。
    62. if (TryLock(Self) > 0) break ;
    63. TEVENT (Inflated enter - Futile wakeup) ;
    64. if (ObjectMonitor::_sync_FutileWakeups != NULL) {
    65. ObjectMonitor::_sync_FutileWakeups->inc() ;
    66. }
    67. ++ nWakeups ;
    68. if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ;
    69. //...
    70. if (_succ == Self) _succ = NULL ;
    71. // Invariant: after clearing _succ a thread *must* retry _owner before parking.
    72. OrderAccess::fence() ;
    73. }
    74. // 退出for循环,说明拿到锁了
    75. // 将当前线程的节点从cxq或者EntryList中移除
    76. UnlinkAfterAcquire (Self, &node) ;
    77. if (_succ == Self) _succ = NULL ;
    78. //...
    79. return ;
    80. }

再次回到刚刚那个问题,自旋是在哪里进行的?
由获取重量级锁的代码可知,在锁膨胀为重量级锁之后,但是在获取互斥量之前,会进行自旋获取锁,如果失败,会将当前线程加入到_cxq队列并挂起等待获取锁。(其实此时还未获取互斥量,那么应该还不算重量级锁,所以说是轻量锁貌似也没啥问题…)

3.4 自旋锁与自适应自旋锁

重量级锁的获取锁和释放锁,需要对线程进行阻塞和唤醒,这种操作需要操作系统来帮忙完成,那么就会涉及到用户态和内核态的转换,进行这种转换是比较耗费性能的。在实际情况下,大多数共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。
为了减少这种挂起和恢复线程的操作,虚拟机使用自旋锁的方式,当一个线程请求一把已经被占用的锁时,并不会立即进入阻塞状态,而是持有CPU执行时间进行一个忙循环,如果在自旋了一定次数还未获得锁,则线程进入阻塞。
自旋锁优点:高效,因为其不会引起上下文切换。
自旋锁缺点:自旋等待会造成CPU资源的浪费。

自适应自旋:自旋的时间有前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待持续更长的时间,比如持续100次忙循环。另一方面,如果对于某个锁,自旋很长成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资源。

自旋锁的源码:TrySpin_VaryDuration(本人c++水平有限,不献丑了)

3.5 锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。

锁消除主要为了减少没有必要的同步操作。
🌰如下:StringBuffer为线程内的对象,其他线程必不可能访问的到,所以append虽然被synchronized修饰,但是可以被安全地消除。

  1. public void test_lock_elimination(){
  2. StringBuffer stringBuffer = new StringBuffer();
  3. stringBuffer.append(1);
  4. stringBuffer.append(1);
  5. stringBuffer.append(1);
  6. }

3.6 锁粗化

锁粗化:若有一系列操作,反复地对同一把锁进行上锁和解锁操作,编译器会扩大这部分代码的同步块的边界,从而只使用一次上锁和解锁操作。

🌰如下:

  1. for (int i = 0; i < 100000; i++) {
  2. synchronized (this) {
  3. // do
  4. }
  5. }
  6. //在锁粗化之后运行逻辑如下列代码
  7. synchronized (this) {
  8. for (int i = 0; i < 100000; i++) {
  9. // do
  10. }
  11. }

4. synchronized和ReentrantLock

区别 synchronized ReentrantLock
底层实现 基于JVM,monitor 基于AQS
手动释放 无需手动释放,在执行完同步代码块或者发生异常时会自动释放锁 需要手动释放
是否可中断 不可中断 可中断
是否公平锁 非公平锁 支持公平锁和非公平锁
是否可绑定条件Condition 不可 可,多个

synchronized和ReentrantLock都可以重入

synchronized的非公平性体现在哪里:当一个线程在获取一把已经被占用的锁时,会先进行自旋获取,失败再进入_cxq挂起。

其他

参考

《Java虚拟机规范8》
《Java并发编程的艺术》
《深入理解java虚拟机》
Synchronized解析——如果你愿意一层一层剥开我的心 https://juejin.cn/post/6844903918653145102
彻底搞懂Java中的偏向锁,轻量级锁,重量级锁 https://www.itqiankun.com/article/bias-lightweight-synchronized-lock
死磕Synchronized底层实现—概论 https://github.com/farmerjohngit/myblog/issues/12