本专栏专一分享大型Bat面试知识,后续会持续更新,喜欢的话麻烦点击一个关注java
面试官:
synchronize关键字在虚拟机执行原理是什么,能谈一谈什么是内存可见性,锁升级吗
心理分析:
面试官必定是想深刻考你并发的内容,看你究竟有没有作过并发处理,大多数开发者在开发App时每每会忽略调并发处理 ,这道题会难住绝大多数人。
求职者:
应该存 锁的执行原理,锁优化 ,和java对象头提及
synchronized的底层是使用操做系统的mutex lock实现的。git
synchronized用的锁是存在Java对象头里的。程序员
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。github
根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,若是这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。若是获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。面试
注意两点:安全
一、synchronized同步快对同一条线程来讲是可重入的,不会出现本身把本身锁死的问题;多线程
二、同步块在已进入的线程执行完以前,会阻塞后面其余线程的进入。并发
监视器锁(Monitor)本质是依赖于底层的操做系统的Mutex Lock(互斥锁)来实现的。每一个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。app
互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,若是互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。编辑器
mutex的工做方式:
因为Java的线程是映射到操做系统的原生线程之上的,若是要阻塞或唤醒一条线程,都须要操做系统来帮忙完成,这就须要从用户态转换到核心态中,所以状态转换须要耗费不少的处理器时间。因此synchronized是Java语言中的一个重量级操做。在JDK1.6中,虚拟机进行了一些优化,譬如在通知操做系统阻塞线程以前加入一段自旋等待过程,避免频繁地切入到核心态中:
synchronized与java.util.concurrent包中的ReentrantLock相比,因为JDK1.6中加入了针对锁的优化措施(见后面),使得synchronized与ReentrantLock的性能基本持平。ReentrantLock只是提供了synchronized更丰富的功能,而不必定有更优的性能,因此在synchronized能实现需求的状况下,优先考虑使用synchronized来进行同步。
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化,以32位的JDK为例:
Synchronized是经过对象内部的一个叫作监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操做系统的Mutex Lock(互斥锁)来实现的。而操做系统实现线程之间的切换须要从用户态转换到核心态,这个成本很是高,状态之间的转换须要相对比较长的时间,这就是为何Synchronized效率低的缘由。所以,这种依赖于操做系统Mutex Lock所实现的锁咱们称之为“重量级锁”。
Java SE 1.6为了减小得到锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”:锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁能够升级但不能降级。
HotSpot的做者通过研究发现,大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到。偏向锁是为了在只有一个线程执行同步块时提升性能。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,之后该线程在进入和退出同步块时不须要进行CAS操做来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的状况下尽可能减小没必要要的轻量级锁执行路径,由于轻量级锁的获取及释放依赖屡次CAS原子指令,而偏向锁只须要在置换ThreadID的时候依赖一次CAS原子指令(因为一旦出现多线程竞争的状况就必须撤销偏向锁,因此偏向锁的撤销操做的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。
偏向锁获取过程:
偏向锁的释放过程:
如上步骤(4)。偏向锁使用了一种等到竞争出现才释放偏向锁的机制:偏向锁只有遇到其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,须要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
关闭偏向锁:
偏向锁在Java 6和Java 7里是默认启用的。因为偏向锁是为了在只有一个线程执行同步块时提升性能,若是你肯定应用程序里全部的锁一般状况下处于竞争状态,能够经过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
轻量级锁是为了在线程近乎交替执行同步块时提升性能。
轻量级锁的加锁过程:
轻量级锁的解锁过程:
如上轻量级锁的加锁过程步骤(5),轻量级锁所适应的场景是线程近乎交替执行同步块的状况,若是存在同一时间访问同一锁的状况,就会致使轻量级锁膨胀为重量级锁。Mark Word的锁标记位更新为10,Mark Word指向互斥量(重量级锁)
Synchronized的重量级锁是经过对象内部的一个叫作监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操做系统的Mutex Lock(互斥锁)来实现的。而操做系统实现线程之间的切换须要从用户态转换到核心态,这个成本很是高,状态之间的转换须要相对比较长的时间,这就是为何Synchronized效率低的缘由。
(具体见前面的mutex lock)
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
锁消除即删除没必要要的加锁操做。虚拟机即时编辑器在运行时,对一些“代码上要求同步,可是被检测到不可能存在共享数据竞争”的锁进行消除。
根据代码逃逸技术,若是判断到一段代码中,堆上的数据不会逃逸出当前线程,那么能够认为这段代码是线程安全的,没必要要加锁。
看下面这段程序:
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的引用没有传递到该方法外,不可能被其余线程拿到该引用),因此其实这过程是线程安全的,能够将锁消除。
若是一系列的连续操做都对同一个对象反复加锁和解锁,甚至加锁操做是出如今循环体中的,那即便没有出现线程竞争,频繁地进行互斥同步操做也会致使没必要要的性能损耗。
若是虚拟机检测到有一串零碎的操做都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操做序列的外部。
举个例子:
public class StringBufferTest { StringBuffer stringBuffer = new StringBuffer(); public void append(){ stringBuffer.append("a"); stringBuffer.append("b"); stringBuffer.append("c"); } }
这里每次调用stringBuffer.append方法都须要加锁和解锁,若是虚拟机检测到有一系列连串的对同一个对象加锁和解锁操做,就会将其合并成一次范围更大的加锁和解锁操做,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
synchronized影响性能的缘由:
二、互斥同步对性能最大的影响是阻塞的实现,由于阻塞涉及到的挂起线程和恢复线程的操做都须要转入内核态中完成(用户态与内核态的切换的性能代价是比较大的)
synchronized锁:对象头中的Mark Word根据锁标志位的不一样而被复用
更多Android高级面试合集放在github上面了
须要的小伙伴能够点击关于我 联系我获取
很是但愿和你们一块儿交流 , 共同进步
也能够扫一扫, 目前是一名程序员,不只分享 Android开发相关知识,同时还分享技术人成长历程,包括我的总结,职场经验,面试经验等,但愿能让你少走一点弯路。