synchronized 是 java 中最经常使用的保证线程安全的方式,synchronized 的做用主要有三方面:html
语义上来说,synchronized主要有三种用法:java
synchronized 同步代码块的语义底层是基于对象内部的监视器锁(monitor),分别是使用 monitorenter 和 monitorexit 指令完成。其实 wait/notify 也依赖于 monitor 对象,因此其通常要在 synchronized 同步的方法或代码块内使用。monitorenter 指令在编译为字节码后插入到同步代码块的开始位置,monitorexit 指令在编译为字节码后插入到方法结束处和异常处。JVM 要保证每一个 monitorenter 必须有对应的 moniorexit。数组
monitorenter:每一个对象都有一个监视器锁(monitor),当 monitor 被某个线程占用时就会处于锁定状态,线程执行 monitorenter 指令时尝试得到 monitor 的全部权,即尝试获取对象的锁。过程以下:安全
monitorexit:执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的全部者。执行指令时,monitor 的进入数减1,若是减1后进入数为0,则线程退出 monitor,再也不是这个 monitor 的全部者,其余被这个 monitor 阻塞的线程能够尝试获取这个 monitor 的全部权。数据结构
在 HotSpot JVM 中,monitor 由 ObjectMonitor 实现,其主要数据结构以下:多线程
ObjectMonitor() { _header = NULL; _count = 0; //记录个数 _waiters = 0, _recursions = 0; _object = NULL; _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 ; }
ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表(每一个等待锁的线程都会被封装成 ObjectWaiter 对象),_owner 指向持有 ObjectMonitor 对象的线程。并发
过程以下图所示: app
在 JDK1.6 以后,出现了各类锁优化技术,如轻量级锁、偏向锁、适应性自旋、锁粗化、锁消除等,这些技术都是为了在线程间更高效的解决竞争问题,从而提高程序的执行效率。ide
经过引入轻量级锁和偏向锁来减小重量级锁的使用。锁的状态总共分四种:无锁状态、偏向锁、轻量级锁和重量级锁。锁随着竞争状况能够升级,但锁升级后不能降级,意味着不能从轻量级锁状态降级为偏向锁状态,也不能从重量级锁状态降级为轻量级锁状态。性能
无锁状态 → 偏向锁状态 → 轻量级锁 → 重量级锁
要理解轻量级锁和偏向锁的运行机制,还要从了解对象头(Object Header)开始。对象头分为两部分:
一、Mark Word:存储对象自身的运行时数据,如:Hash Code,GC 分代年龄、锁信息。这部分数据在32位和64位的 JVM 中分别为 32bit 和 64bit。考虑空间效率,Mark Word 被设计为非固定的数据结构,以便在极小的空间内存储尽可能多的信息,32bit的 Mark Word 以下图所示:
二、存储指向方法区对象类型数据的指针,若是是数组对象的话,额外会存储数组的长度
monitor 监视器锁本质上是依赖操做系统的 Mutex Lock 互斥量 来实现的,咱们通常称之为重量级锁
。由于 OS 实现线程间的切换须要从用户态转换到核心态,这个转换过程成本较高,耗时相对较长,所以 synchronized 效率会比较低。
重量级锁的锁标志位为'10',指针指向的是 monitor 对象的起始地址,关于 monitor 的实现原理上文已经描述了。
轻量级锁
是相对基于OS的互斥量实现的重量级锁而言的,它的本意是在没有多线程竞争的前提下,减小传统的重量级锁使用OS的互斥量而带来的性能消耗。
轻量级锁提高性能的经验依据是:对于绝大部分锁,在整个同步周期内都是不存在竞争的
。若是没有竞争,轻量级锁就可使用 CAS 操做避免互斥量的开销,从而提高效率。
轻量级锁的加锁过程:
一、线程在进入到同步代码块的时候,JVM 会先在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象当前 Mark Word 的拷贝(官方称为 Displaced Mark Word),owner 指针指向对象的 Mark Word。此时堆栈与对象头的状态如图所示:
二、JVM 使用 CAS 操做尝试将对象头中的 Mark Word 更新为指向 Lock Record 的指针。若是更新成功,则执行步骤3;更新失败,则执行步骤4
三、若是更新成功,那么这个线程就拥有了该对象的锁,对象的 Mark Word 的锁状态为轻量级锁(标志位转变为'00')。此时线程堆栈与对象头的状态如图所示:
四、若是更新失败,JVM 首先检查对象的 Mark Word 是否指向当前线程的栈帧
轻量级锁
就要升级为重量级锁
(锁的标志位转变为'10'),Mark Word 中存储的就是指向重量级锁的指针,后面等待锁的线程也就进入阻塞状态轻量级锁的解锁过程:
一、经过 CAS 操做用线程中复制的 Displaced Mark Word 中的数据替换对象当前的 Mark Word
二、若是替换成功,整个同步过程就完成了
三、若是替换失败,说明有其余线程尝试过获取该锁,那就在释放锁的同时,唤醒被挂起的线程
轻量级锁
是在无多线程竞争的状况下,使用 CAS 操做去消除互斥量;偏向锁
是在无多线程竞争的状况下,将这个同步都消除掉。
偏向锁提高性能的经验依据是:对于绝大部分锁,在整个同步周期内不只不存在竞争,并且总由同一线程屡次得到
。偏向锁会偏向第一个得到它的线程,若是接下来的执行过程当中,该锁没有被其余线程获取,则持有偏向锁的线程不须要再进行同步。这使得线程获取锁的代价更低。
偏向锁的获取过程:
一、线程执行同步块,锁对象第一次被获取的时候,JVM 会将锁对象的 Mark Word 中的锁状态设置为偏向锁(锁标志位为'01',是否偏向的标志位为'1'),同时经过 CAS 操做在 Mark Word 中记录获取到这个锁的线程的 ThreadID
二、若是 CAS 操做成功。持有偏向锁的线程每次进入和退出同步块时,只需测试一下 Mark Word 里是否存储着当前线程的 ThreadID。若是是,则表示线程已经得到了锁,而不须要额外花费 CAS 操做加锁和解锁
三、若是不是,则经过CAS操做竞争锁,竞争成功,则将 Mark Word 的 ThreadID 替换为当前线程的 ThreadID
偏向锁的释放过程:
一、当一个线程已经持有偏向锁,而另一个线程尝试竞争偏向锁时,CAS 替换 ThreadID 操做失败,则开始撤销偏向锁。偏向锁的撤销,须要等待原持有偏向锁的线程到达全局安全点(在这个时间点上没有字节码正在执行),暂停该线程,并检查其状态
二、若是原持有偏向锁的线程不处于活动状态或已退出同步代码块,则该线程释放锁。将对象头设置为无锁状态(锁标志位为'01',是否偏向标志位为'0')
三、若是原持有偏向锁的线程未退出同步代码块,则升级为轻量级锁(锁标志位为'00')
偏向锁、轻量级锁、重量级锁之间的状态转换如图所示(归纳上文描述的锁获取和释放的内容):
下面是这几种锁的比较:
一、适应性自旋
自旋锁
:互斥同步时,挂起和恢复线程都须要切换到内核态完成,这对性能并发带来了很多的压力。同时在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段较短的时间而去挂起和恢复线程并不值得。那么若是有多个线程同时并行执行,可让后面请求锁的线程经过自旋(CPU忙循环执行空指令)的方式稍等一下子,看看持有锁的线程是否会很快的释放锁,这样就不须要放弃 CPU 的执行时间了。
适应性自旋
:在轻量级锁获取过程当中,线程执行 CAS 操做失败时,须要经过自旋来获取重量级锁。若是锁被占用的时间比较短,那么自旋等待的效果就会比较好,而若是锁占用的时间很长,自旋的线程则会白白浪费 CPU 资源。解决这个问题的最简答的办法就是:指定自旋的次数,若是在限定次数内还没获取到锁(例如10次),就按传统的方式挂起线程进入阻塞状态。JDK1.6 以后引入了自适应性自旋的方式,若是在同一锁对象上,一线程自旋等待刚刚成功得到锁,而且持有锁的线程正在运行中,那么 JVM 会认为此次自旋也有可能再次成功得到锁,进而容许自旋等待相对更长的时间(例如100次)。另外一方面,若是某个锁自旋不多成功得到,那么之后要得到这个锁时将省略自旋过程,以免浪费 CPU。
二、锁消除
锁消除就是编译器运行时,对一些被检测到不可能存在共享数据竞争的锁进行消除。若是判断一段代码中,堆上的数据不会逃逸出去从而被其余线程访问到,则能够把他们当作栈上的数据对待,认为它们是线程私有的,没必要要加锁。
public String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append("a"); sb.append("b"); sb.append("c"); return sb.toString(); }
在 StringBuffer.append() 方法中有一个同步代码块,锁就是sb对象,但 sb 的全部引用不会逃逸到 concatString() 方法外部,其余线程没法访问它。所以这里有锁,可是在即时编译以后,会被安全的消除掉,忽略掉同步而直接执行了。
三、锁粗化
锁粗化就是 JVM 检测到一串零碎的操做都对同一个对象加锁,则会把加锁同步的范围粗化到整个操做序列的外部。以上述 concatString() 方法为例,内部的 StringBuffer.append() 每次都会加锁,将会锁粗化,在第一次 append() 前至 最后一个 append() 后只须要加一次锁就能够了。
《深刻理解Java虚拟机》- 周志明
Java Synchronised机制
Java synchronized 关键字的实现原理
---
个人博客即将搬运同步至腾讯云+社区,邀请你们一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=12mihsfip6v9b