深刻分析Synchronized原理(阿里面试题)

还有一篇 讲解lock的实现原理,参考:解决多线程安全问题-无非两个方法synchronized和lock 具体原理以及如何 获取锁AQS算法 (百度-美团)html

 

记得开始学习Java的时候,一遇到多线程状况就使用synchronized,相对于当时的咱们来讲synchronized是这么的神奇而又强大,那个时候咱们赋予它一个名字“同步”,也成为了咱们解决多线程状况的百试不爽的良药。可是,随着学习的进行咱们知道在JDK1.5以前synchronized是一个重量级锁,相对于j.u.c.Lock,它会显得那么笨重,以致于咱们认为它不是那么的高效而慢慢摒弃它。java

不过,随着Javs SE 1.6对synchronized进行的各类优化后,synchronized并不会显得那么重了。下面来一块儿探索synchronized的基本使用、实现机制、Java是如何对它进行了优化、锁优化机制、锁的存储结构等升级过程。程序员

1 基本使用

Synchronized是Java中解决并发问题的一种最经常使用的方法,也是最简单的一种方法。Synchronized的做用主要有三个:算法

  1. 原子性:确保线程互斥的访问同步代码;
  2. 可见性:保证共享变量的修改可以及时可见,实际上是经过Java内存模型中的 “对一个变量unlock操做以前,必需要同步到主内存中;若是对一个变量进行lock操做,则将会清空工做内存中此变量的值,在执行引擎使用此变量前,须要从新从主内存中load操做或assign操做初始化变量值” 来保证的;
  3. 有序性:有效解决重排序问题,即 “一个unlock操做先行发生(happen-before)于后面对同一个锁的lock操做”;

从语法上讲,Synchronized能够把任何一个非null对象做为"锁",在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)数组

Synchronized总共有三种用法:缓存

  1. 当synchronized做用在实例方法时,监视器锁(monitor)即是对象实例(this);
  2. 当synchronized做用在静态方法时,监视器锁(monitor)即是对象的Class实例,由于Class数据存在于永久代,所以静态方法锁至关于该类的一个全局锁;
  3. 当synchronized做用在某一个对象实例时,监视器锁(monitor)即是括号括起来的对象实例;

注意,synchronized 内置锁 是一种 对象锁(锁的是对象而非引用变量),做用粒度是对象 ,能够用来实现对 临界资源的同步互斥访问 ,是 可重入 的。其可重入最大的做用是避免死锁,如:安全

子类同步方法调用了父类同步方法,如没有可重入的特性,则会发生死锁;数据结构

2 同步原理

数据同步须要依赖锁,那锁的同步又依赖谁?synchronized给出的答案是在软件层面依赖JVM,而j.u.c.Lock给出的答案是在硬件层面依赖特殊的CPU指令。多线程

当一个线程访问同步代码块时,首先是须要获得锁才能执行同步代码,当退出或者抛出异常时必需要释放锁,那么它是如何来实现这个机制的呢?咱们先看一段简单的代码:架构

package com.paddx.test.concurrent;
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

查看反编译后结果: 

反编译结果
  1. monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的全部权,过程以下:

    1. 若是monitor的进入数为0,则该线程进入monitor,而后将进入数设置为1,该线程即为monitor的全部者;
    2. 若是线程已经占有该monitor,只是从新进入,则进入monitor的进入数加1;
    3. 若是其余线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再从新尝试获取monitor的全部权;
  2. monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的全部者。指令执行时,monitor的进入数减1,若是减1后进入数为0,那线程退出monitor,再也不是这个monitor的全部者。其余被这个monitor阻塞的线程能够尝试去获取这个 monitor 的全部权。

    monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;

经过上面两段描述,咱们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是经过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为何只有在同步的块或者方法中才能调用wait/notify等方法,不然会抛出java.lang.IllegalMonitorStateException的异常的缘由。

再来看一下同步方法:

package com.paddx.test.concurrent;

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

查看反编译后结果: 

反编译结果 

从编译的结果来看,方法的同步并无经过指令 monitorenter 和 monitorexit 来完成(理论上其实也能够经过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的:

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,若是设置了,执行线程将先获取monitor,获取成功以后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其余任何线程都没法再得到同一个monitor对象。

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需经过字节码来完成。两个指令的执行是JVM经过调用操做系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待从新调度,会致使“用户态和内核态”两个态之间来回切换,对性能有较大影响。

3 同步概念

3.1 Java对象头

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。以下图所示:

 
 
  1. 实例数据:存放类的属性数据信息,包括父类的属性信息;
  2. 对齐填充:因为虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
  3. 对象头:Java对象头通常占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),可是 若是对象是数组类型,则须要3个机器码,由于JVM虚拟机能够经过Java对象的元数据信息肯定Java对象的大小,可是没法从数组的元数据来确认数组的大小,因此用一块来记录数组长度。

Synchronized用的锁就是存在Java对象头里的,那么什么是Java对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。其中 Class Pointer是对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。 Java对象头具体结构描述以下:

 
Java对象头结构组成

Mark Word用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。好比锁膨胀就是借助Mark Word的偏向的线程ID 参考:JAVA锁的膨胀过程和优化(阿里) 阿里也常常问的问题

下图是Java对象头 无锁状态下Mark Word部分的存储结构(32位虚拟机):

 
Mark Word存储结构 

对象头信息是与对象自身定义的数据无关的额外存储成本,可是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽可能多的数据,它会根据对象的状态复用本身的存储空间,也就是说,Mark Word会随着程序的运行发生变化,可能变化为存储如下4种数据:

 
Mark Word可能存储4种数据

在64位虚拟机下,Mark Word是64bit大小的,其存储结构以下:

 
64位Mark Word存储结构

对象头的最后两位存储了锁的标志位,01是初始状态,未加锁,其对象头里存储的是对象自己的哈希码,随着锁级别的不一样,对象头里会存储不一样的内容。偏向锁存储的是当前占用此对象的线程ID;而轻量级则存储指向线程栈中锁记录的指针。从这里咱们能够看到,“锁”这个东西,多是个锁记录+对象头里的引用指针(判断线程是否拥有锁时将线程的锁记录地址和对象头里的指针地址比较),也多是对象头里的线程ID(判断线程是否拥有锁时将线程的ID和对象头里存储的线程ID比较)。 

HotSpot虚拟机对象头Mark Word

3.2 对象头中Mark Word与线程中Lock Record

在线程进入同步代码块的时候,若是此同步对象没有被锁定,即它的锁标志位是01,则虚拟机首先在当前线程的栈中建立咱们称之为“锁记录(Lock Record)”的空间,用于存储锁对象的Mark Word的拷贝,官方把这个拷贝称为Displaced Mark Word。整个Mark Word及其拷贝相当重要。

Lock Record是线程私有的数据结构,每个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程的惟一标识(或者object mark word),表示该锁被这个线程占用。以下图所示为Lock Record的内部结构:

Lock Record 描述
Owner 初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程惟一标识,当锁被释放时又设置为NULL;
EntryQ 关联一个系统互斥锁(semaphore),阻塞全部试图锁住monitor record失败的线程;
RcThis 表示blocked或waiting在该monitor record上的全部线程的个数;
Nest 用来实现 重入锁的计数;
HashCode 保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate 用来避免没必要要的阻塞或等待线程唤醒,由于每一次只有一个线程可以成功拥有锁,若是每次前一个释放锁的线程唤醒全部正在阻塞或等待的线程,会引发没必要要的上下文切换(从阻塞到就绪而后由于竞争锁失败又被阻塞)从而致使性能严重降低。Candidate只有两种可能的值0表示没有须要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

3.3 监视器(Monitor)

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不同,可是均可以经过成对的MonitorEnter和MonitorExit指令来实现。 

  1. MonitorEnter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的全部权,即尝试得到该对象的锁;
  2. MonitorExit指令:插入在方法结束处和异常处,JVM保证每一个MonitorEnter必须有对应的MonitorExit;

那什么是Monitor?能够把它理解为 一个同步工具,也能够描述为 一种同步机制,它一般被 描述为一个对象。

与一切皆对象同样,全部的Java对象是天生的Monitor,每个Java对象都有成为Monitor的潜质,由于在Java的设计中 ,每个Java对象自打娘胎里出来就带了一把看不见的锁,它叫作内部锁或者Monitor锁

也就是一般说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构以下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,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 ;
  }

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每一个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

  1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
  2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
  3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其余线程进入获取monitor(锁);

同时,Monitor对象存在于每一个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁即是经过这种方式获取锁的,也是为何Java中任意对象能够做为锁的缘由,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,因此必须在同步代码块中使用。

监视器Monitor有两种同步方式:互斥与协做。多线程环境下线程之间若是须要共享数据,须要解决互斥访问数据的问题,监视器能够确保监视器上的数据在同一时刻只会有一个线程在访问。

何时须要协做? 好比:

一个线程向缓冲区写数据,另外一个线程从缓冲区读数据,若是读线程发现缓冲区为空就会等待,当写线程向缓冲区写入数据,就会唤醒读线程,这里读线程和写线程就是一个合做关系。JVM经过Object类的wait方法来使本身等待,在调用wait方法后,该线程会释放它持有的监视器,直到其余线程通知它才有执行的机会。一个线程调用notify方法通知在等待的线程,这个等待的线程并不会立刻执行,而是要通知线程释放监视器后,它从新获取监视器才有执行的机会。若是恰好唤醒的这个线程须要的监视器被其余线程抢占,那么这个线程会继续等待。Object类中的notifyAll方法能够解决这个问题,它能够唤醒全部等待的线程,总有一个线程执行。

 

如上图所示,一个线程经过1号门进入Entry Set(入口区),若是在入口区没有线程等待,那么这个线程就会获取监视器成为监视器的Owner,而后执行监视区域的代码。若是在入口区中有其它线程在等待,那么新来的线程也会和这些线程一块儿等待。线程在持有监视器的过程当中,有两个选择,一个是正常执行监视器区域的代码,释放监视器,经过5号门退出监视器;还有可能等待某个条件的出现,因而它会经过3号门到Wait Set(等待区)休息,直到相应的条件知足后再经过4号门进入从新获取监视器再执行。

注意:

当一个线程释放监视器时,在入口区和等待区的等待线程都会去竞争监视器,若是入口区的线程赢了,会从2号门进入;若是等待区的线程赢了会从4号门进入。只有经过3号门才能进入等待区,在等待区中的线程只有经过4号门才能退出等待区,也就是说一个线程只有在持有监视器时才能执行wait操做,处于等待的线程只有再次得到监视器才能退出等待状态。

4 锁的优化

从JDK5引入了现代操做系统新增长的CAS原子操做( JDK5中并无对synchronized关键字作优化,而是体如今J.U.C中,因此在该版本concurrent包有更好的性能 ),从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋以外,还增长了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。因为此关键字的优化使得性能极大提升,同时语义清晰、操做简单、无需手动关闭,因此推荐在容许的状况下尽可能使用此关键字,同时在性能上此关键字还有优化的空间。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,锁能够从偏向锁升级到轻量级锁,再升级的重量级锁。可是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

在 JDK 1.6 中默认是开启偏向锁和轻量级锁的,能够经过-XX:-UseBiasedLocking来禁用偏向锁。

4.1 自旋锁

线程的阻塞和唤醒须要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来讲是一件负担很重的工做,势必会给系统的并发性能带来很大的压力。同时咱们发如今许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是很是不值得的。

因此引入自旋锁,何谓自旋锁? 

所谓自旋锁,就是指当一个线程尝试获取某个锁时,若是该锁已被其余线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

自旋锁适用于锁保护的临界区很小的状况,临界区很小的话,锁占用的时间就很短。自旋等待不能替代阻塞,虽然它能够避免线程切换带来的开销,可是它占用了CPU处理器的时间。若是持有锁的线程很快就释放了锁,那么自旋的效率就很是好,反之,自旋的线程就会白白消耗掉处理的资源,它不会作任何有意义的工做,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。因此说,自旋等待的时间(自旋的次数)必需要有一个限度,若是自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。

自旋锁在JDK 1.4.2中引入,默认关闭,可是可使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,能够经过参数-XX:PreBlockSpin来调整。

若是经过参数-XX:PreBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如将参数调整为10,可是系统不少线程都是等你刚刚退出的时候就释放了锁(假如多自旋一两次就能够获取锁),是否是很尴尬。因而JDK1.6引入自适应的自旋锁,让虚拟机会变得愈来愈聪明。

4.2 适应性自旋锁

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数再也不是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。那它如何进行适应性自旋呢? 

线程若是自旋成功了,那么下次自旋的次数会更加多,由于虚拟机认为既然上次成功了,那么这次自旋也颇有可能会再次成功,那么它就会容许自旋等待持续的次数更多。反之,若是对于某个锁,不多有自旋可以成功,那么在之后要或者这个锁的时候自旋的次数会减小甚至省略掉自旋过程,以避免浪费处理器资源。

有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的情况预测会愈来愈准确,虚拟机会变得愈来愈聪明。

4.3 锁消除

为了保证数据的完整性,在进行操做时须要对这部分操做进行同步控制,可是在有些状况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。

锁消除的依据是逃逸分析的数据支持

若是不存在竞争,为何还须要加锁呢?因此锁消除能够节省毫无心义的请求锁的时间。变量是否逃逸,对于虚拟机来讲须要使用数据流分析来肯定,可是对于程序员来讲这还不清楚么?在明明知道不存在数据竞争的代码块前加上同步吗?可是有时候程序并非咱们所想的那样?虽然没有显示使用锁,可是在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操做。好比StringBuffer的append()方法,Vector的add()方法:

public void vectorTest(){
    Vector<String> vector = new Vector<String>();
    for(int i = 0 ; i < 10 ; i++){
        vector.add(i + "");
    }

    System.out.println(vector);
}

在运行这段代码时,JVM能够明显检测到变量vector没有逃逸出方法vectorTest()以外,因此JVM能够大胆地将vector内部的加锁操做消除。

4.4 锁粗化

在使用同步锁的时候,须要让同步块的做用范围尽量小—仅在共享数据的实际做用域中才进行同步,这样作的目的是 为了使须要同步的操做数量尽量缩小,若是存在锁竞争,那么等待锁的线程也能尽快拿到锁。

在大多数的状况下,上述观点是正确的。可是若是一系列的连续加锁解锁操做,可能会致使没必要要的性能损耗,因此引入锁粗话的概念。

锁粗话概念比较好理解,就是将多个连续的加锁、解锁操做链接在一块儿,扩展成一个范围更大的锁

如上面实例:

vector每次add的时候都须要加锁操做,JVM检测到对同一个对象(vector)连续加锁、解锁操做,会合并一个更大范围的加锁、解锁操做,即加锁解锁操做会移到for循环以外。

4.5 偏向锁

偏向锁是JDK6中的重要引进,由于HotSpot做者通过研究实践发现,在大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到,为了让线程得到锁的代价更低,引进了偏向锁。

偏向锁是在单线程执行代码块时使用的机制,若是在多线程并发的环境下(即线程A还没有执行完同步代码块,线程B发起了申请锁的申请),则必定会转化为轻量级锁或者重量级锁。

在JDK5中偏向锁默认是关闭的,而到了JDK6中偏向锁已经默认开启。若是并发数较大同时同步代码块执行时间较长,则被多个线程同时访问的几率就很大,就可使用参数-XX:-UseBiasedLocking来禁止偏向锁(但这是个JVM参数,不能针对某个对象锁来单独设置)。

引入偏向锁主要目的是:为了在没有多线程竞争的状况下尽可能减小没必要要的轻量级锁执行路径。由于轻量级锁的加锁解锁操做是须要依赖屡次CAS原子指令的,而偏向锁只须要在置换ThreadID的时候依赖一次CAS原子指令(因为一旦出现多线程竞争的状况就必须撤销偏向锁,因此偏向锁的撤销操做的性能损耗也必须小于节省下来的CAS原子指令的性能消耗)。

轻量级锁是为了在线程交替执行同步块时提升性能,而偏向锁则是在只有一个线程执行同步块时进一步提升性能。

那么偏向锁是如何来减小没必要要的CAS操做呢?首先咱们看下无竞争下锁存在什么问题:

如今几乎全部的锁都是可重入的,即已经得到锁的线程能够屡次锁住/解锁监视对象,按照以前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操做(好比对等待队列的CAS操做),CAS操做会延迟本地调用,所以偏向锁的想法是 一旦线程第一次得到了监视对象,以后让监视对象“偏向”这个线程,以后的屡次调用则能够避免CAS操做,说白了就是置个变量,若是发现为true则无需再走各类加锁/解锁流程。

CAS为何会引入本地延迟?这要从SMP(对称多处理器)架构提及,下图大概代表了SMP的结构:

 
SMP(对称多处理器)架构

其意思是 全部的CPU会共享一条系统总线(BUS),靠此总线链接主存。每一个核都有本身的一级缓存,各核相对于BUS对称分布,所以这种结构称为“对称多处理器”。

而CAS的全称为Compare-And-Swap,是一条CPU的原子指令,其做用是让CPU比较后原子地更新某个位置的值,通过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类即是使用了这些封装后的接口。

例如:Core1和Core2可能会同时把主存中某个位置的值Load到本身的L1 Cache中,当Core1在本身的L1 Cache中修改这个位置的值时,会经过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现本身L1 Cache中的值失效(称为Cache命中缺失)则会经过总线从内存中加载该地址最新的值,你们经过总线的来回通讯称为“Cache一致性流量”,由于总线被设计为固定的“通讯能力”,若是Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致性”,从这个层面来讲,锁设计的终极目标即是减小Cache一致性流量。

而CAS刚好会致使Cache一致性流量,若是有不少线程都共享同一个对象,当某个Core CAS成功时必然会引发总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,下降Cache一致性流量。

Cache一致性:

上面提到Cache一致性,实际上是有协议支持的,如今通用的协议是MESI(最先由Intel开始支持),具体参考:http://en.wikipedia.org/wiki/MESI_protocol

Cache一致性流量的例外状况:

其实也不是全部的CAS都会致使总线风暴,这跟Cache一致性协议有关,具体参考:http://blogs.oracle.com/dave/entry/biased_locking_in_hotspot

NUMA(Non Uniform Memory Access Achitecture)架构:

与SMP对应还有非对称多处理器架构,如今主要应用在一些高端处理器上,主要特色是没有总线,没有公用主存,每一个Core有本身的内存,针对这种结构此处不作讨论。

因此,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,之后该线程进入和退出同步块时不须要花费CAS操做来争夺锁资源,只须要检查是否为偏向锁、锁标识为以及ThreadID便可,处理流程以下:

  1. 检测Mark Word是否为可偏向状态,便是否为偏向锁1,锁标识位为01;
  2. 若为可偏向状态,则测试线程ID是否为当前线程ID,若是是,则执行步骤(5),不然执行步骤(3);
  3. 若是测试线程ID不为当前线程ID,则经过CAS操做竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,不然执行线程(4);
  4. 经过CAS竞争锁失败,证实当前存在多线程竞争状况,当到达全局安全点,得到偏向锁的线程被挂起,偏向锁升级为轻量级锁,而后被阻塞在安全点的线程继续往下执行同步代码块;
  5. 执行同步代码块;

偏向锁的释放采用了 一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,须要等待其余线程来竞争。偏向锁的撤销须要 等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤以下:

  1. 暂停拥有偏向锁的线程;
  2. 判断锁对象是否还处于被锁定状态,否,则恢复到无锁状态(01),以容许其他线程竞争。是,则挂起持有锁的当前线程,并将指向当前线程的锁记录地址的指针放入对象头Mark Word,升级为轻量级锁状态(00),而后恢复持有锁的当前线程,进入轻量级锁的竞争模式;

注意:此处将 当前线程挂起再恢复的过程当中并无发生锁的转移,仍然在当前线程手中,只是穿插了个 “将对象头中的线程ID变动为指向锁记录地址的指针” 这么个事。

 
偏向锁的获取和释放过程

4.6 轻量级锁

引入轻量级锁的主要目的是 在没有多线程竞争的前提下,减小传统的重量级锁使用操做系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁致使偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤以下:

  1. 在线程进入同步块时,若是同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。此时线程堆栈与对象头的状态以下图所示:

     
    轻量级锁CAS操做以前线程堆栈与对象的状态
  2. 拷贝对象头中的Mark Word复制到锁记录(Lock Record)中;

  3. 拷贝成功后,虚拟机将使用CAS操做尝试将对象Mark Word中的Lock Word更新为指向当前线程Lock Record的指针,并将Lock record里的owner指针指向object mark word。若是更新成功,则执行步骤(4),不然执行步骤(5);

  4. 若是这个更新动做成功了,那么当前线程就拥有了该对象的锁,而且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,此时线程堆栈与对象头的状态以下图所示:

     
    轻量级锁CAS操做以后线程堆栈与对象的状态
  5. 若是这个更新操做失败了,虚拟机首先会检查对象Mark Word中的Lock Word是否指向当前线程的栈帧,若是是,就说明当前线程已经拥有了这个对象的锁,那就能够直接进入同步块继续执行。不然说明多个线程竞争锁,进入自旋执行(3),若自旋结束时仍未得到锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。

轻量级锁的释放也是经过CAS操做来进行的,主要步骤以下:

  1. 经过CAS操做尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word;
  2. 若是替换成功,整个同步过程就完成了,恢复到无锁状态(01);
  3. 若是替换失败,说明有其余线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程;

对于轻量级锁,其性能提高的依据是 “对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,若是打破这个依据则除了互斥的开销外,还有额外的CAS操做,所以在有多线程竞争的状况下,轻量级锁比重量级锁更慢。

 
轻量级锁的获取和释放过程
  1. 为何升级为轻量锁时要把对象头里的Mark Word复制到线程栈的锁记录中呢?

    由于在申请对象锁时 须要以该值做为CAS的比较条件,同时在升级到重量级锁的时候,能经过这个比较断定是否在持有锁的过程当中此锁被其余线程申请过,若是被其余线程申请了,则在释放锁的时候要唤醒被挂起的线程。

  2. 为何会尝试CAS不成功以及什么状况下会不成功?

    CAS自己是不带锁机制的,其是经过比较而来。假设以下场景:线程A和线程B都在对象头里的锁标识为无锁状态进入,那么如线程A先更新对象头为其锁记录指针成功以后,线程B再用CAS去更新,就会发现此时的对象头已经不是其操做前的对象HashCode了,因此CAS会失败。也就是说,只有两个线程并发申请锁的时候会发生CAS失败。

    而后线程B进行CAS自旋,等待对象头的锁标识从新变回无锁状态或对象头内容等于对象HashCode(由于这是线程B作CAS操做前的值),这也就意味着线程A执行结束(参见后面轻量级锁的撤销,只有线程A执行完毕撤销锁了才会重置对象头),此时线程B的CAS操做终于成功了,因而线程B得到了锁以及执行同步代码的权限。若是线程A的执行时间较长,线程B通过若干次CAS时钟没有成功,则锁膨胀为重量级锁,即线程B被挂起阻塞、等待从新调度。

此处,如何理解“轻量级”?“轻量级”是相对于使用操做系统互斥量来实现的传统锁而言的。可是,首先须要强调一点的是,轻量级锁并非用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减小传统的重量级锁使用产生的性能消耗。

轻量级锁所适应的场景是线程交替执行同步块的状况,若是存在同一时间访问同一锁的状况,必然就会致使轻量级锁膨胀为重量级锁。

4.7 重量级锁

Synchronized是经过对象内部的一个叫作 监视器锁(Monitor)来实现的。可是监视器锁本质又是依赖于底层的操做系统的Mutex Lock来实现的。而操做系统实现线程之间的切换这就须要从用户态转换到核心态,这个成本很是高,状态之间的转换须要相对比较长的时间,这就是为何Synchronized效率低的缘由。所以,这种依赖于操做系统Mutex Lock所实现的锁咱们称之为 “重量级锁”。

4.8 重量级锁、轻量级锁和偏向锁之间转换

 
重量级锁、轻量级锁和偏向锁之间转换
 
Synchronized偏向锁、轻量级锁及重量级锁转换流程

5 锁的优劣

各类锁并非相互代替的,而是在不一样场景下的不一样选择,绝对不是说重量级锁就是不合适的。每种锁是只能升级,不能降级,即由偏向锁->轻量级锁->重量级锁,而这个过程就是开销逐渐加大的过程。

  1. 若是是单线程使用,那偏向锁毫无疑问代价最小,而且它就能解决问题,连CAS都不用作,仅仅在内存中比较下对象头就能够了;
  2. 若是出现了其余线程竞争,则偏向锁就会升级为轻量级锁;
  3. 若是其余线程经过必定次数的CAS尝试没有成功,则进入重量级锁;

在第3种状况下进入同步代码块就 要作偏向锁创建、偏向锁撤销、轻量级锁创建、升级到重量级锁,最终仍是得靠重量级锁来解决问题,那这样的代价就比直接用重量级锁要大很多了。因此使用哪一种技术,必定要看其所处的环境及场景,在绝大多数的状况下,偏向锁是有效的,这是基于HotSpot做者发现的“大多数锁只会由同一线程并发申请”的经验规律。

 
锁的优劣

6 扩展资料

  1. JVM源码分析之synchronized实现
  2. 自旋锁、排队自旋锁、MCS锁、CLH锁
  3. 深刻理解Java并发之synchronized实现原理 

 

参考:啃碎并发(七):深刻分析Synchronized原理

还有一篇写的不错:深刻理解synchronized底层原理,一篇文章就够了!

另外一篇少的:synchronize原理