深入HotSpot虚拟机源码探究synchronized底层实现原理【万字总结synchronized】

在这里插入图片描述


一、synchronized原理

synchronized是Java中的关键字,无法通过JDK源码查看它的实现,它是由JVM提供支持的,所以如果想要了解具体的实现需要查看JVM源码

(1)首先准备好HotSpot源码

jdk8 hotspot源码下载地址:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/

选择zip或者gz格式下载即可
在这里插入图片描述

(2)解压,使用vscode或者其他编辑器打开

在这里插入图片描述
src是hotspot的源码目录

cpu:和cpu相关的一些操作
os:在不同操作系统上的一些区别操作
os_cpu:关联os和cpu的实现
share:公共代码

  • tools:一些工具类
  • vm:公共源码

(3)初始monitor监视器锁(先了解后细说)

相信都对下面几行代码非常熟悉,如果不熟悉synchronized的底层的话,可能会直接认为这个锁是依赖Object对象的。这当然是无稽之谈!

        Object lock = new Object();
        synchronized (lock) {
            
        }

其实无论是synchronized代码块还是synchronized方法,其锁的实现最终依赖monitor监视器(先记住这个概念后面细说);那么你是否头上有个大大的问号,那么这个Object对象有什么用呢
在这里插入图片描述
其实这要从对象头中的MarkWord说起了(这里我长话短说,后面细说);每个Java对象在内存中包含了三部分数据对象头、实例数据、对齐填充

对象头:包含了markword(状态标志位)和类元信息指针、数组长度(如果对象是数组则多这一项)
实例数据:存放具体的实例变量数据
对齐填充:JVM要求Java对象分配内存必须是8的倍数(不满足8的倍数时填充一些字节)

重点在markword ! ! ! ! !
在这里插入图片描述
这个markword的状态是动态变化的(节省空间),分为四种状态-无锁、偏向锁、轻量级锁、重量级锁;某一时刻Object的状态只能处于其中一种,这应该没什么疑问吧。这个动态变化涉及到了锁优化(锁升级、锁粗化、锁消除),这个概念先了解,后面细说!!!

重点来了 !!!!
Monitor被翻译成"监视器",可以理解为实现同步的一种工具,通常被描述为一个对象,Java中每个对象都关联着一把“看不见的锁”,,为什么?看完这段描述你就明白了!当我们使用synchronized给对象上锁之后(注意这里假设认为是重量级锁),该对象中的markword字段是处于重量级锁状态,然后它会被设置指向Monitor对象的指针(Monitor由C++实现)

这个monitor不是我们创建的,而是JVM执行到同步代码块时创建的,monitor里面有两个重要的变量,分别是owner(占有锁的线程),recursions(线程获取锁的次数)

(4)建立宏观概念(初始基本流程)

打开HotSpot源码文件src/share/vm/runtime/ObjectMonitor.hpp

在hotspot中,monitor是由ObjectMonitor对象来实现的,找到该对象对应的构造器
在这里插入图片描述
首先描述一下这个核心流程(只看核心部分)
在这里插入图片描述

Owner:持有monitor的线程,对应上面源码中的_object变量
WaitSet:处于等待状态的线程会被放到该队列(例如调用wait()方法),对应上面源码中的_WaitSet变量
EntryList:当多个线程竞争锁时,竞争锁失败的线程会被放入到该队列,处于阻塞状态,需要唤醒。对应上面源码中的_EntryList变量

(5)分析锁竞争源码

	synchronized (lock) {
    	num++;
	}

上面的同步代码经过反编译之后得到如下字节码指令
在这里插入图片描述
想必都听说过monitorenter和monitorexit指令,一个表示获取监视器锁,一个表示释放监视器锁;

关于锁竞争的JVM源码,最终会调用到MonitorObject类中的enter()方法

void ATTR ObjectMonitor::enter(TRAPS) {
  Thread * const Self = THREAD ;
  void * cur ;
//通过CAS操作尝试将_owner变量设置为当前线程,如果_owner为NULL表示锁未被占用
//CAS:内存值、预期值、新值,只有当内存值==预期值,才能将新值替换内存值
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  if (cur == NULL) { //如果未NULL,表示获取锁成功,直接返回即可
     assert (_recursions == 0   , "invariant") ;
     assert (_owner      == Self, "invariant") ;
     return ;
  }
//线程重入,synchronized的可重入特性原理,_owner保存的线程与当前正在执行的线程相同,将_recursions++
  if (cur == Self) { 
     _recursions ++ ;
     return ;
  }
//表示线程第一次进入monitor,则进行一些设置
  if (Self->is_lock_owned ((address)cur)) {
    assert (_recursions == 0, "internal state error");
    _recursions = 1 ;  //锁的次数设置为1
    _owner = Self ;  //将_owner设置为当前线程
    OwnerIsThread = 1 ;  
    return ;
  }
  .....
  .....省略
  .....
    //获取锁失败
    for (;;) {
      jt->set_suspend_equivalent();
    //等待锁的释放
      EnterI (THREAD) ;

      if (!ExitSuspendEquivalent(jt)) break ;

      _recursions = 0 ;
      _succ = NULL ;
      exit (false, Self) ;

      jt->java_suspend_self();
    }
    Self->set_current_pending_monitor(NULL);
  }
}

总结下来,也就是四步骤

Ⅰ、通过CAS尝试将_owner变量设置为当前线程
Ⅱ、如果是线程重入(下面有举例),则将_recurisons++
Ⅲ、如果线程是第一次进入,则将_recurisons设置为1,将_owner设置为当前线程,该线程获取锁成功并返回
Ⅳ、如果获取锁失败,则等待锁的释放

  synchronized (lock) {
     num++;
     synchronized (lock) {  //锁重入_recurisons+1
                        
      }
  }

(6)分析锁等待源码

在锁竞争源码中最后一步,如果获取锁失败,则等待锁的释放,由MonitorObject类中的EnterI()方法来实现

void ATTR ObjectMonitor::EnterI (TRAPS) {
    Thread * Self = THREAD ;
    assert (Self->is_Java_thread(), "invariant") ;
    assert (((JavaThread *) Self)->thread_state() == _thread_blocked   , "invariant") ;
  //再次尝试获取锁,获取成功直接返回
    if (TryLock (Self) > 0) {
		....
        return ;
    }

    DeferredInitialize () ;


  //尝试自旋获取锁,获取锁成功直接返回
    if (TrySpin (Self) > 0) {
        ....
        return ;
    }

  //前面的尝试都失败,则将该线程信息封装到node节点
    ObjectWaiter node(Self) ;
    Self->_ParkEvent->reset() ;
    node._prev   = (ObjectWaiter *) 0xBAD ;
    node.TState  = ObjectWaiter::TS_CXQ ;

  
    ObjectWaiter * nxt ;
    //将node节点插入到_cxq的头部,前面说过锁获取失败的线程首先会进入_cxq
    //_cxq是一个单链表,等到一轮过去在该_cxq列表中的线程还未成功获取锁,
    //则进入_EntryList列表
    for (;;) {                  //注意这里的死循环操作
        node._next = nxt = _cxq ;
        //这里插入节点时也使用了CAS,因为可能有多个线程失败将加入_cxq链表
        if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;


        //如果线程CAS插入_cxq链表失败,它会再抢救一下看看能不能获取到锁
        if (TryLock (Self) > 0) {
            ...
            return ;
        }
    }


    //竞争减弱时,将该线程设置为_Responsible(负责线程),定时轮询_owner
    //后面该线程会调用定时的park方法,防止死锁
    if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
        Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
    }


    TEVENT (Inflated enter - Contention) ;
    int nWakeups = 0 ;
    int RecheckInterval = 1 ;
    //前面获取锁失败的线程已经放入到了_cxq列表,但还未挂起
    //下面是将_cxq列表挂起的代码,线程一旦挂起,必须唤醒之后才能继续操作
    for (;;) {
        //挂起之前,再次尝试获取锁,看看能不能成功,成功则跳出循环
        if (TryLock (Self) > 0) break ;
        assert (_owner != Self, "invariant") ;

        if ((SyncFlags & 2) && _Responsible == NULL) {
           Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
        }
        //将当前线程挂起(park()方法)
        // park self
        //如果当前线程是_Responsible线程,则调用定时的park方法,防止死锁
        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) ;
            Self->_ParkEvent->park() ;
        }
        //当线程被唤醒之后,会再次尝试获取锁
        if (TryLock(Self) > 0) break ;
		//唤醒锁之后,还出现竞争,记录唤醒次数,这里的计数器
		//并没有受锁的保护,也没有原子更新,为了获取更低的探究影响
        TEVENT (Inflated enter - Futile wakeup) ;
        if (ObjectMonitor::_sync_FutileWakeups != NULL) {
           ObjectMonitor::_sync_FutileWakeups->inc() ;
        }
        ++ nWakeups ; //唤醒次数

        //自旋尝试获取锁
        if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ;


        if ((Knob_ResetEvent & 1) && Self->_ParkEvent->fired()) {
           Self->_ParkEvent->reset() ;
           OrderAccess::fence() ;
        }
        if (_succ == Self) _succ = NULL ;

        // Invariant: after clearing _succ a thread *must* retry _owner before parking.
        OrderAccess::fence() ;
    }

    //已经获取到了锁,将当前节点从_EntryList队列中删除
    UnlinkAfterAcquire (Self, &node) ;
    if (_succ == Self) _succ = NULL ;

  	...
    return ;
}

总结下来也就一下几步:

  • 首先tryLock再次尝试获取锁,之后再CAS尝试获取锁;失败后将当前线程信息封装成ObjectWaiter对象。
  • 在for(;;)循环中,通过CAS将该节点插入到_cxq链表的头部(这个时刻可能有多个获取锁失败的线程要插入),CAS插入失败的线程再次尝试获取锁
  • 如果还没获取到锁,则将线程挂起;等待唤醒
  • 当线程被唤醒时,再次尝试获取锁

我能从这个源码设计理念中学到什么?

看完这个锁等待源码,你是不是有了一个疑问,为什么使用了多次tryLock尝试获取锁和CAS获取锁?源码中无限推迟了线程的挂起操作,你可以看到从开始到线程挂起的代码中,出现了多次的尝试获取锁;因为线程的挂起与唤醒涉及到了状态的转换(内核态和用户态),这种频繁的切换必定会给系统带来性能上的瓶颈。所以它的设计意图就是尽量推辞线程的挂起时间,取一个极限的时间挂起线程。
另外源码中定义了负责线程_Responsible,这种标识的线程调用的是定时的park(线程挂起),避免死锁

你永远也不知道在某个时刻你全部的线程会不会同时挂起,所以最好的解决办法就是:设计一种Responsible负责线程,让它一直活跃或者定时醒来。

(7)分析锁释放源码

void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
   Thread * Self = THREAD ; 
   if (THREAD != _owner) {  //判断当前线程是否是线程持有者
    //当前线程是之前持有轻量级锁的线程。由轻量级锁膨胀后还没调用过enter方法,_owner会是指向Lock Record的指针
     if (THREAD->is_lock_owned((address) _owner)) {

       assert (_recursions == 0, "invariant") ;
       _owner = THREAD ;
       _recursions = 0 ;
       OwnerIsThread = 1 ;
     } else {  //当前线程不是锁的持有者--》出现异常
       TEVENT (Exit - Throw IMSX) ;
       assert(false, "Non-balanced monitor enter/exit!");
       if (false) {
          THROW(vmSymbols::java_lang_IllegalMonitorStateException());
       }
       return;
     }
   }
  //重入,计数器-1,返回
   if (_recursions != 0) {
     _recursions--;        // this is simple recursive enter
     TEVENT (Inflated exit - recursive) ;
     return ;
   }

   //_Responsible设置为NULL
   if ((SyncFlags & 4) == 0) {
      _Responsible = NULL ;
   }

#if INCLUDE_JFR
   if (not_suspended && EventJavaMonitorEnter::is_enabled()) {
    _previous_owner_tid = JFR_THREAD_ID(Self);
   }
#endif

   for (;;) {
      assert (THREAD == _owner, "invariant") ;


      if (Knob_ExitPolicy == 0) {
       
         //先释放锁,这时如果有其他线程获取锁,则能获取到
         OrderAccess::release_store_ptr (&_owner, NULL) ;   // drop the lock
         OrderAccess::storeload() ;                         // See if we need to wake a successor
         //等待队列为空,或者有"醒着的线程”,则不需要去等待队列唤醒线程了,直接返回即可
         if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
            TEVENT (Inflated exit - simple egress) ;
            return ;
         }
         TEVENT (Inflated exit - complex egress) ;

 		 //当前线程重新获取锁,因为后序要唤醒队列
 		 //一旦获取失败,说明有线程获取到锁了,直接返回即可,不需要获取锁再去唤醒线程了
         if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
            return ;
         }
         TEVENT (Exit - Reacquired) ;
      } else {
         if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
            OrderAccess::release_store_ptr (&_owner, NULL) ;   // drop the lock
            OrderAccess::storeload() ;
            // Ratify the previously observed values.
            if (_cxq == NULL || _succ != NULL) {
                TEVENT (Inflated exit - simple egress) ;
                return ;
            }
             //当前线程重新获取锁,因为后序要唤醒队列
              //一旦获取失败,说明有线程获取到锁了,直接返回即可,不需要获取锁再去唤醒线程了
            if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
               TEVENT (Inflated exit - reacquired succeeded) ;
               return ;
            }
            TEVENT (Inflated exit - reacquired failed) ;
         } else {
            TEVENT (Inflated exit - complex egress) ;
         }
      }

      guarantee (_owner == THREAD, "invariant") ;

      ObjectWaiter * w = NULL ;
      int QMode = Knob_QMode ;   //根据QMode的不同,会有不同的唤醒策略

      if (QMode == 2 && _cxq != NULL) {
        //QMode==2,_cxq中有优先级更高的线程,直接唤醒_cxq的队首线程
		.........
          return ;
      }
      //当QMode=3的时候 讲_cxq中的数据加入到_EntryList尾部中来 然后从_EntryList开始获取
      if (QMode == 3 && _cxq != NULL) {   
          .....
          }
		....... //省略
		.......
      //当QMode=4的时候 讲_cxq中的数据加入到_EntryList前面来 然后从_EntryList开始获取
      if (QMode == 4 && _cxq != NULL) {
			......
          }
          
        //批量修改状态标志改成TS_ENTER
          ObjectWaiter * q = NULL ;
          ObjectWaiter * p ;
          for (p = w ; p != NULL ; p = p->_next) {
              guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
              p->TState = ObjectWaiter::TS_ENTER ;
              p->_prev = q ;
              q = p ;
          }
        //插到原有的_EntryList前面 从员_EntryList中获取
          // Prepend the RATs to the EntryList
          if (_EntryList != NULL) {
              q->_next = _EntryList ;
              _EntryList->_prev = q ;
          }
          _EntryList = w ;

      }
	..........
	..........省略
   }
}

核心流程如下:

1.将_recursions减1,_owner置空
2.如果队列中等待的线程为空或者_succ不为空(有"醒着的线程”,则不需要取唤醒线程了),直接返回即可。
3.第二条不满足,当前线程重新获取锁,去唤醒线程
4.唤醒线程,根据QMode的不同,有不同的唤醒策略

QMode = 2且cxq非空:cxq中有优先级更高的线程,直接唤醒_cxq的队首线程
QMode = 3且cxq非空:把cxq队列插入到EntryList的尾部;
QMode = 4且cxq非空:把cxq队列插入到EntryList的头部;
QMode = 0:暂时什么都不做,继续往下看;

只有QMode=2的时候会提前返回,等于0、3、4的时候都会继续往下执行:

我能从这个源码设计理念中学到什么?

首先在它的锁释放源码中,首先就将锁释放,然后再去判断是否有醒着的线程;如果不满足再让该线程重新获取锁去唤醒线程。如何理解它设计理念的精髓之处,首先先将锁释放,因为可能有线程正在尝试或者自旋获取锁,然后 TODO:


(8)为什么synchronized是重量级锁?

从上面的ObjectMonitor类中的函数调用设计到了Atom:cmpxchg_ptr、Atom:inc_ptr等内核函数,没有获取到锁的线程会被挂起,竞争到锁的线程会被唤醒;这涉及到了状态的转换,即内核态和用户态的转换,浪费资源。
在这里插入图片描述
内核:控制计算机的硬件资源,为上层应用程序提供服务
系统调用:内核给上层应用提供的接口,为了能够访问到硬件资源
用户空间:用户程序执行的空间

系统调用的具体过程如下:

1.用户态程序将一些参数数据放在寄存器或者堆栈中,表明需要
2.用户态程序系统调用
3.CPU切换到内核态,并跳转到指定位置的指令
4.读取寄存器或者堆栈中的数据参数,执行相应的请求服务
5.完成系统调用,切换到用户态并返回系统调用结果

在这里插入图片描述
从上面可以看出系统调用设计到了参数的传递,同时还需要保存切换前用户态下的状态,这种频繁的切换无疑给系统带来了性能上的瓶颈;所以JDK6 synchronized进行了优化


二、synchronized的锁及其优化

在JDK5及其之前,只有重量级锁,在JDK6实现了几种锁优化技术,锁升级、锁消除、锁粗化,提高了synchronized的效率。

无锁->偏向锁->轻量级锁->重量级锁

在这里插入图片描述

(1)初识偏向锁

顾名思义就是"偏向"第一个获取锁的线程,会在markword中存储该线程的id;以后该线程进入和退出同步代码块只需要检查是否为偏向锁、锁标志位和线程id即可。

public class TendencyTest {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}
class MyThread extends Thread {
    private static Object lock = new Object();
    
    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    }
}

上面这段简单的代码就是使用偏向锁的场景,查看执行结果:可以看出刚好是1和01表明是当前处于偏向锁。
在这里插入图片描述
偏向锁原理:

1.当线程第一次获取锁时,虚拟机会将是否为偏向锁设置为1,将锁标志位设置为01;等到以后该线程再来访问同步代码块时,不需要再进行任何同步操作
2.偏向锁的撤销恢复到无锁或者轻量级锁状态,需要在全局安全点才能撤销

(2)初识轻量级锁

顾名思义,轻量级锁就是相对于重量级锁而言的,它并不是用来代替重量级锁的,引入的目的是为了在多线程交互的场景下,避免重量级锁带来的性能消耗。

轻量级锁原理:
(1)首先判断当前对象是否处于无锁,如果是,则JVM将在当前线程栈帧中创建一个Lock Record,用于存储对象目前的markword的拷贝
在这里插入图片描述

(2)让Lock Record中owner指向锁对象,CAS尝试将MarkWord更新为指向Lock Record的指针,将MarkWord的数据存入Lock Records
在这里插入图片描述
(3)如果CAS成功,对象的Mark Word将会存储Lock Record 地址 和 锁状态 00
在这里插入图片描述
(4)如果CAS失败,此时会有两种情况:
如果是其他线程已经持有了该轻量级锁,则表示发生竞争,此时进入锁膨胀。
如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数,只不过新添加的Lock Record中没有Object的Mark word内容,为null。
在这里插入图片描述
(5)当退出 synchronized 代码块,如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一,将null的Lock Record删除。
当退出 synchronized 代码块,锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头。
成功,则解锁成功。
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

(3)初始自旋锁,查看HotSpot中自旋锁的实现

当某个线程获取锁,CPU一直被其他线程占用着,就一直循环检测锁是否被释放,而不是进入线程阻塞状态。自旋锁是一种基于CAS的一种锁,它依赖CPU的空转,每一次自旋通常会暂停一段时间;它适用于线程执行时间较短的场景,在这种场景下CPU的空转开销是远小于线程切换的。

一句话总结自旋锁:自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。

JDK6推出了新的自适应自旋锁。自适应意味着自旋的时间不再固定了,而是由于上一个锁拥有者自旋获取锁的时间所决定。(比如上一个获取锁之前自旋的时间为1s,那么这次可能就是1.2s,比上一次长一点)

查看自旋锁在HotSpot的实现

int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) {

  //原始的自旋锁
    int ctr = Knob_FixedSpin ;
    if (ctr != 0) { //当自旋次数不等于0时
        while (--ctr >= 0) {  --操作
            if (TryLock (Self) > 0) return 1 ; //每次自旋尝试获取锁
            SpinPause () ;                           //自旋一次暂停一段时间
        }
        return 0 ;
    }
    //新版自适应自旋
    for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) { // Knob_PreSpin默认是自旋10次
      if (TryLock(Self) > 0) {                     //每次自旋尝试获取锁
    
        int x = _SpinDuration ;
        if (x < Knob_SpinLimit) {
           if (x < Knob_Poverty) x = Knob_Poverty ;
           _SpinDuration = x + Knob_BonusB ;    //如果获取锁成功,修改一下自旋的时间,允许比上次长一点
        }
        return 1 ;
      }
      SpinPause () ;  //自旋一次暂停的时间
    }

(4)锁消除

锁消除即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。根据代码逃逸技术分析,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么就可以认为这段代码是线程安全的,无需加锁。

    public String back(String str1, String str2) {
   
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(str1).append(str2).toString();
    }

StringBuffer的append代码如下:

     @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

看上面这几行代码,首先分析StringBuffer中append同步方法锁的是哪个对象?肯定是当前new的StringBuffer对象,由于每个线程来执行back方法时,都会创建一个StringBuffer对象,所以它的锁对象是不同的;另外可以发现这个StringBuffer并没有逃逸出这个方法。所以可以进行锁消除,将 synchronized去掉。

(5)锁粗化

假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

下面代码示例:

public class StringBufferTest {
 
    StringBuffer stringBuffer = new StringBuffer();
 
    public void append(){
        stringBuffer.append("a").append("b").append("c");
    }

上述代码每次调用 stringBuffer.append 方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

(6)锁升级

偏向锁->轻量级锁->重量解锁

Ⅰ.偏向锁->轻量级锁:当某个线程首次去获取锁时,会将MarkWord中的线程id设置为当前线程id;后续再有线程来尝试获取锁时,需要MarkWord中的线程id和当前线程id是否一致,如果一致就无需CAS来加锁解锁;如果不一致需要判断MarkWord记录的线程是否存活,如果不存活,重置锁状态为无锁,其他线程可以设置为偏向锁;如果存活,找到它对应的栈帧信息,检测该线程是否还需要继续持有锁,如果需要,则暂停它,撤销偏行锁,膨胀为轻量级锁;如果不需要则重置锁状态为无锁。

Ⅱ.轻量级锁->重量级锁:当线程1获取轻量级锁,首先会在该线程1栈帧中开辟一段Displaced MarkWord空间,然后将对象头中的MarkWord复制到Displaced MarkWord,将对象头中MarkWord的的地址替换为Displaced MarkWord的地址
这时线程2通过CAS方式来获取锁,将MarkWord到线程2的锁记录空间;之后发现锁已经被线程1获取了,那么它就会通过自旋的方式等待锁的释放。
这个自旋是有次数的,默认是10次(源码注释说明20-100最适合),也提供了自适应自旋时间,上面自旋锁已经解释过。
一旦超过了自旋次数,那么会撤销轻量级锁,膨胀为重量级锁。

注意只能锁升级,不能锁降级。偏向锁可以重置为无锁。

(7)平常写代码如何对synchronized进行优化

1.减少同步代码块中的内容,缩短执行时间

   synchronized (lock) {
      num++;
   }

2.降低锁粒度

将锁拆分为多个锁,降低锁粒度;最为著名的就是Hastable和ConcurrentHashMap做对比,Hashtable锁住的是整个哈希表,效率低下;ConcurrentHashMap在JDK8之前使用了锁分段技术,锁住的是Segment段,JDK8更是将锁的粒度降低到了Node级别,使用CAS+Synchronized锁住根节点。

3.读写锁分离

读时不加锁,写入时才加锁。


三、synchronized的五大特性

1.Synchronized保证原子性

public class SynchronizedTest {
    private static int num;
    private static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (lock) { //同步代码块
                    num++;
                }
            }
        };
        List<Thread> list = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            Thread t = new Thread(runnable);
            t.start();
            list.add(t);
        }
        for (Thread t : list) {
            t.join();
        }
        System.out.println("num:" + num);
    }
}

经过反编译查看字节码指令
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-amlNXZbf-1652164856682)(https://note.youdao.com/yws/res/5620/WEBRESOURCEf0c84e3b848d713982799fa3887dc2db)]

同步代码块反编译后的字节码指令如上图,其中monitorenter和monitorexit这两个JVM指令是同步代码块实现的核心,monitorenter表示获取监视器锁,monitorexit表示释放监视器锁。当某个线程获取锁之后,其他线程必须等待该线程释放锁,才能执行同步代码块中的内容。

2.Synchronized保证可见性

public class SynchronizedTest02 {
    private static boolean flag = true;
    private static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {
                synchronized (lock) {

                }
            }
        }).start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(() -> {
            flag = false;
            System.out.println("修改了flag变量为false");
        }).start();
    }
}

使用了synchronized同步代码块之后,程序能够正常结束,无论是Synchronized还是Volatile都是使用memory barrier来保证可见性的。

monitorenter指令之后会有一个Load屏障,重新拉取被别的线程修改后的值;monitorexit指令之前会有一个Store屏障,将自身修改后的数据刷新到高速缓冲或主内存中。

3.Synchronized保证有序性

public class SynchronizedTest03 {
    private static int num = 0;
    private static boolean flag = true;
    private static final Object lock = new Object();
    public static void main(String[] args) {
        
        new Thread(() -> {
            synchronized (lock) {
                num++;
                flag = false;
            }
        }).start();
    }
}

Synchronized也是通过内存屏障来保证有序性的,通过Acquire Barrier和Release Barrier来实现。

Acquire Barrier在一个读操作之后插入,禁止该读操作和以后的任何读写操作发生重排序
Release Barrier在一个写操作之前插入,禁止该写操作与任何读写操作发生重排序

4.Synchronized的可重入特性

public class SynchronizedTest04 {
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (SynchronizedTest.class) {
                System.out.println(Thread.currentThread().getName() + "进入同步代码块一");
                synchronized (SynchronizedTest.class) {
                    System.out.println(Thread.currentThread().getName() + "进入同步代码块二");
                }
            }
        }).start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D7dHb20g-1652164856683)(https://note.youdao.com/yws/res/5684/WEBRESOURCEa5aff264bc3e485b41599f35f1e46fe9)]

Synchronized的锁对象中有一个计数器,会记录线程获得锁的次数,每次获取锁,计数器加1,每次释放锁,计数器减1,当计数器为0时,完成释放;能够避免死锁,方便使用其他方法进行封装

5.Synchronized不可中断特性

public class Synchronized05 {
    public static void main(String[] args) throws InterruptedException {

        Runnable runnable = () -> {
            synchronized (Synchronized05.class) {
                System.out.println(Thread.currentThread().getName() + "正在执行同步代码块...");
                TimeUnit.SECONDS.sleep(10);
            }
        };
        new Thread(runnable).start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(runnable).start();
    }
}

某个线程获取锁之后,其他线程处于阻塞或者等待状态。

在这里插入图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thecoastlines

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值