腾讯面试必问:不能逃避的synchronize关键字。

本专栏专一分享大型Bat面试知识,后续会持续更新,喜欢的话麻烦点击一个关注java

面试官:
synchronize关键字在虚拟机执行原理是什么,能谈一谈什么是内存可见性,锁升级吗
心理分析
面试官必定是想深刻考你并发的内容,看你究竟有没有作过并发处理,大多数开发者在开发App时每每会忽略调并发处理 ,这道题会难住绝大多数人。
求职者:
应该存 锁的执行原理,锁优化 ,和java对象头提及

一.锁的内存语义

synchronized的底层是使用操做系统的mutex lock实现的。git

  • 内存可见性:同步快的可见性是由“若是对一个变量执行lock操做,将会清空工做内存中此变量的值,在执行引擎使用这个变量前须要从新执行load或assign操做初始化变量的值”、“对一个变量执行unlock操做以前,必须先把此变量同步回主内存中(执行store和write操做)”这两条规则得到的。
  • 操做原子性:持有同一个锁的两个同步块只能串行地进入
锁的内存语义:
  • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
锁释放和锁获取的内存语义:
  • 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所作修改的)消息。
  • 线程B获取一个锁,实质上是线程B接收了以前某个线程发出的(在释放这个锁以前对共享变量所作修改的)消息。
  • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A经过主内存向线程B发送消息

二.synchronized锁

synchronized用的锁是存在Java对象头里的。程序员

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。github

根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,若是这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。若是获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。面试

注意两点:安全

一、synchronized同步快对同一条线程来讲是可重入的,不会出现本身把本身锁死的问题;多线程

二、同步块在已进入的线程执行完以前,会阻塞后面其余线程的进入。并发

三.Mutex Lock

监视器锁(Monitor)本质是依赖于底层的操做系统的Mutex Lock(互斥锁)来实现的。每一个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。app

互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,若是互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。编辑器

mutex的工做方式:

  • 申请mutex
  • 若是成功,则持有该mutex
  • 若是失败,则进行spin自旋. spin的过程就是在线等待mutex, 不断发起mutex gets, 直到得到mutex或者达到spin_count限制为止
  • 依据工做模式的不一样选择yiled仍是sleep
  • 若达到sleep限制或者被主动唤醒或者完成yield, 则重复1)~4)步,直到得到为止

因为Java的线程是映射到操做系统的原生线程之上的,若是要阻塞或唤醒一条线程,都须要操做系统来帮忙完成,这就须要从用户态转换到核心态中,所以状态转换须要耗费不少的处理器时间。因此synchronized是Java语言中的一个重量级操做。在JDK1.6中,虚拟机进行了一些优化,譬如在通知操做系统阻塞线程以前加入一段自旋等待过程,避免频繁地切入到核心态中:

synchronized与java.util.concurrent包中的ReentrantLock相比,因为JDK1.6中加入了针对锁的优化措施(见后面),使得synchronized与ReentrantLock的性能基本持平。ReentrantLock只是提供了synchronized更丰富的功能,而不必定有更优的性能,因此在synchronized能实现需求的状况下,优先考虑使用synchronized来进行同步。

四.Java对象头


在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化,以32位的JDK为例:

五.锁优化

偏向锁、轻量级锁、重量级锁

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

Java SE 1.6为了减小得到锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”:锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁能够升级但不能降级。

5.1 偏向锁

HotSpot的做者通过研究发现,大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到。偏向锁是为了在只有一个线程执行同步块时提升性能。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,之后该线程在进入和退出同步块时不须要进行CAS操做来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的状况下尽可能减小没必要要的轻量级锁执行路径,由于轻量级锁的获取及释放依赖屡次CAS原子指令,而偏向锁只须要在置换ThreadID的时候依赖一次CAS原子指令(因为一旦出现多线程竞争的状况就必须撤销偏向锁,因此偏向锁的撤销操做的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。

偏向锁获取过程:

  • (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
  • (2)若是为可偏向状态,则测试线程ID是否指向当前线程,若是是,进入步骤(5),不然进入步骤(3)。
  • (3)若是线程ID并未指向当前线程,则经过CAS操做竞争锁。若是竞争成功,则将Mark Word中线程ID设置为当前线程ID,而后执行(5);若是竞争失败,执行(4)。
  • (4)若是CAS获取偏向锁失败,则表示有竞争(CAS获取偏向锁失败说明至少有过其余线程曾经得到过偏向锁,由于线程不会主动去释放偏向锁)。当到达全局安全点(safepoint)时,会首先暂停拥有偏向锁的线程,而后检查持有偏向锁的线程是否活着(由于可能持有偏向锁的线程已经执行完毕,可是该线程并不会主动去释放偏向锁),若是线程不处于活动状态,则将对象头设置成无锁状态(标志位为“01”),而后从新偏向新的线程;若是线程仍然活着,撤销偏向锁后升级到轻量级锁状态(标志位为“00”),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待得到该轻量级锁。
  • (5)执行同步代码。

偏向锁的释放过程:

如上步骤(4)。偏向锁使用了一种等到竞争出现才释放偏向锁的机制:偏向锁只有遇到其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,须要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

关闭偏向锁:

偏向锁在Java 6和Java 7里是默认启用的。因为偏向锁是为了在只有一个线程执行同步块时提升性能,若是你肯定应用程序里全部的锁一般状况下处于竞争状态,能够经过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

5.2轻量级锁

轻量级锁是为了在线程近乎交替执行同步块时提升性能。

轻量级锁的加锁过程:

  • (1)在代码进入同步块的时候,若是同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态以下图所示。
  • (2)拷贝对象头中的Mark Word复制到锁记录中。
  • (3)拷贝成功后,虚拟机将使用CAS操做尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。若是更新成功,则执行步骤(3),不然执行步骤(4)。
  • (4)若是这个更新动做成功了,那么这个线程就拥有了该对象的锁,而且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态以下图所示。

  • (5)若是这个更新操做失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,若是是就说明当前线程已经拥有了这个对象的锁,那就能够直接进入同步块继续执行。不然说明多个线程竞争锁,若当前只有一个等待线程,则可经过自旋稍微等待一下,可能另外一个线程很快就会释放锁。 可是当自旋超过必定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程之外的线程都阻塞,防止CPU空转,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

轻量级锁的解锁过程:

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

如上轻量级锁的加锁过程步骤(5),轻量级锁所适应的场景是线程近乎交替执行同步块的状况,若是存在同一时间访问同一锁的状况,就会致使轻量级锁膨胀为重量级锁。Mark Word的锁标记位更新为10,Mark Word指向互斥量(重量级锁)

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

(具体见前面的mutex lock)

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



偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。

  • 一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它如今认为只可能有一个线程来访问它,因此当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操做,并将对象头中的ThreadID改为本身的ID,以后再次访问这个对象时,只须要对比ID,不须要再使用CAS在进行操做。
  • 一旦有第二个线程访问这个对象,由于偏向锁不会主动释放,因此第二个线程能够看到对象时偏向状态,这时代表在这个对象上已经存在竞争了。检查原来持有该对象锁的线程是否依然存活,若是挂了,则能够将对象变为无锁状态,而后从新偏向新的线程。若是原来的线程依然存活,则立刻执行那个线程的操做栈,检查该对象的使用状况,若是仍然须要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待得到该轻量级锁;若是不存在使用了,则能够将对象回复成无锁状态,而后从新偏向。
  • 轻量级锁认为竞争存在,可是竞争的程度很轻,通常两个线程对于同一个锁的操做都会错开,或者说稍微等待一下(自旋),另外一个线程就会释放锁。 可是当自旋超过必定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程之外的线程都阻塞,防止CPU空转。

六其余锁优化

6.1锁消除

锁消除即删除没必要要的加锁操做。虚拟机即时编辑器在运行时,对一些“代码上要求同步,可是被检测到不可能存在共享数据竞争”的锁进行消除。

根据代码逃逸技术,若是判断到一段代码中,堆上的数据不会逃逸出当前线程,那么能够认为这段代码是线程安全的,没必要要加锁。

看下面这段程序:

public class SynchronizedTest {

    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();

        for (int i = 0; i < 100000000; i++) {
            test.append("abc", "def");
        }
    }

    public void append(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }
}

虽然StringBuffer的append是一个同步方法,可是这段程序中的StringBuffer属于一个局部变量,而且不会从该方法中逃逸出去(即StringBuffer sb的引用没有传递到该方法外,不可能被其余线程拿到该引用),因此其实这过程是线程安全的,能够将锁消除。

6.2锁粗化

若是一系列的连续操做都对同一个对象反复加锁和解锁,甚至加锁操做是出如今循环体中的,那即便没有出现线程竞争,频繁地进行互斥同步操做也会致使没必要要的性能损耗。

若是虚拟机检测到有一串零碎的操做都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操做序列的外部。

举个例子:

public class StringBufferTest {
    StringBuffer stringBuffer = new StringBuffer();

    public void append(){
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
    }
}

这里每次调用stringBuffer.append方法都须要加锁和解锁,若是虚拟机检测到有一系列连串的对同一个对象加锁和解锁操做,就会将其合并成一次范围更大的加锁和解锁操做,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

6.3自旋锁与自适应自旋锁

  • 引入自旋锁的缘由:互斥同步对性能最大的影响是阻塞的实现,由于挂起线程和恢复线程的操做都须要转入内核态中完成,这些操做给系统的并发性能带来很大的压力。同时虚拟机的开发团队也注意到在许多应用上面,共享数据的锁定状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是很是不值得的。
  • 自旋锁:让该线程执行一段无心义的忙循环(自旋)等待一段时间,不会被当即挂起(自旋不放弃处理器额执行时间),看持有锁的线程是否会很快释放锁。自旋锁在JDK 1.4.2中引入,默认关闭,可是可使用-XX:+UseSpinning开开启;在JDK1.6中默认开启。
  • 自旋锁的缺点:自旋等待不能替代阻塞,虽然它能够避免线程切换带来的开销,可是它占用了处理器的时间。若是持有锁的线程很快就释放了锁,那么自旋的效率就很是好;反之,自旋的线程就会白白消耗掉处理器的资源,它不会作任何有意义的工做,这样反而会带来性能上的浪费。因此说,自旋等待的时间(自旋的次数)必需要有一个限度,例如让其循环10次,若是自旋超过了定义的时间仍然没有获取到锁,则应该被挂起(进入阻塞状态)。经过参数-XX:PreBlockSpin能够调整自旋次数,默认的自旋次数为10。
  • 自适应的自旋锁:JDK1.6引入自适应的自旋锁,自适应就意味着自旋的次数再也不是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:若是在同一个锁的对象上,自旋等待刚刚成功得到过锁,而且持有锁的线程正在运行中,那么虚拟机就会认为此次自旋也颇有可能再次成功,进而它将容许自旋等待持续相对更长的时间。若是对于某个锁,自旋不多成功得到过,那在之后要获取这个锁时将可能省略掉自旋过程,以免浪费处理器资源。简单来讲,就是线程若是自旋成功了,则下次自旋的次数会更多,若是自旋失败了,则自旋的次数就会减小。
  • 自旋锁使用场景:从轻量级锁获取的流程中咱们知道,当线程在获取轻量级锁的过程当中执行CAS操做失败时,是要经过自旋来获取重量级锁的。(见前面“轻量级锁”)

七.总结

  • synchronized特色:保证内存可见性、操做原子性
  • synchronized影响性能的缘由:

    • 一、加锁解锁操做须要额外操做;

二、互斥同步对性能最大的影响是阻塞的实现,由于阻塞涉及到的挂起线程和恢复线程的操做都须要转入内核态中完成(用户态与内核态的切换的性能代价是比较大的)

  • synchronized锁:对象头中的Mark Word根据锁标志位的不一样而被复用

    • 偏向锁:在只有一个线程执行同步块时提升性能。Mark Word存储锁偏向的线程ID,之后该线程在进入和退出同步块时不须要进行CAS操做来加锁和解锁,只需简单比较ThreadID。特色:只有等到线程竞争出现才释放偏向锁,持有偏向锁的线程不会主动释放偏向锁。以后的线程竞争偏向锁,会先检查持有偏向锁的线程是否存活,若是不存货,则对象变为无锁状态,从新偏向;若是仍存活,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待得到该轻量级锁
    • 轻量级锁:在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,尝试拷贝锁对象目前的Mark Word到栈帧的Lock Record,若拷贝成功:虚拟机将使用CAS操做尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向对象的Mark Word。若拷贝失败:若当前只有一个等待线程,则可经过自旋稍微等待一下,可能持有轻量级锁的线程很快就会释放锁。 可是当自旋超过必定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁
    • 重量级锁:指向互斥量(mutex),底层经过操做系统的mutex lock实现。等待锁的线程会被阻塞,因为Linux下Java线程与操做系统内核态线程一一映射,因此涉及到用户态和内核态的切换、操做系统内核态中的线程的阻塞和恢复。

关于我

更多Android高级面试合集放在github上面了

须要的小伙伴能够点击关于我 联系我获取

很是但愿和你们一块儿交流 , 共同进步

也能够扫一扫, 目前是一名程序员,不只分享 Android开发相关知识,同时还分享技术人成长历程,包括我的总结,职场经验,面试经验等,但愿能让你少走一点弯路。

相关文章
相关标签/搜索