java的对象头
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:
对象头
对象头又包括两部分信息,第一部分用于存储对象自身的运行时数据(Mark Word),如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。对象头的另外一部分是类型指针(Klass pointer),即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
值得注意的是:类元信息存在于方法区,类元信息有区别与堆中的synchClass字节码对象,synchClass可以理解为类加载完成后,JVM将类的信息存在堆中,然后使用反射去访问其全部信息(包括函数和字段),然而在JVM内部大多数对象都是使用C++代码实现的,对于JVM内部如果需要类信息,JVM就会通过对象头的类型指针去拿方法区中类元信息的数据。
实例数据:存放类的属性数据信息,包括父类的属性信息。
对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
synchronized作用的锁
synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit。如下图所示:
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下图所示:
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据,如下图所示:
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下图所示:
指针压缩
现在虚拟机基本是64位的,而64位的对象头有点浪费空间,JVM默认会开启指针压缩,所以基本上也是按32位的形式记录对象头的。也可以通过下面参数进行控制JVM开启和关闭指针压缩:
- 开启压缩指针(-XX:+UseCompressedOops)
- 关闭压缩指针(-XX:-UseCompressedOops)
为什么JVM需要默认开启指针压缩呢?
原因在于在对象头上类元信息指针Klass pointer在32位JVM虚拟机中用4个字节存储,但是到了64位JVM虚拟机中Klass pointer用的就是8个字节来存储,一些对象在32位虚拟机用的也是4字节来存储,到了64位机器用的都是8字节来存储了,一个工程项目中有成千上万的对象,倘若每个对象都用8字节来存放的话,那这些对象无形中就会增加很多空间,导致堆的压力就会很大,堆很容易就会满了,然后就会更容易的触发GC,那指针压缩的最主要的作用就是压缩每个对象内存地址的大小,那么同样堆内存大小就可以放更多的对象。
为什么JVM中的对象由年轻代进入老年代的默认分代年龄是15? 对象头中有4个字节用于存放对象分代年龄的,4个字节就是2的四次方等于16,其范围就是0~15,所以也就很好理解对象在GC的时候,JVM对象由年轻代进入老年代的默认分代年龄是15了。
synchronized的基础用法
synchronized 通过当前线程持有对象锁,从而拥有访问权限,而其他没有持有当前对象锁的线程无法拥有访问权限,保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,从而保证线程安全。synchronized 可以保证线程的可见性,synchronized 属于隐式锁,锁的持有与释放都是隐式的,我们无需干预。synchronized最主要的三种应用方式:
-
修饰实例(类)方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁。如果多个线程不同对象访问该方法,则无法保证同步。
-
修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。锁的是包含这个方法的类,也就是类对象,这样如果多个线程不同对象访问该静态方法,也是可以保证同步的。
-
修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
在应用Sychronized关键字时需要把握如下注意点:
- 一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
- 每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁
- synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁
synchronized修饰代码块
方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象)。
手动指定锁定对象,也可是是this,也可以是自定义的锁
public class SynchronizedObjectLock implements Runnable{
static SynchronizedObjectLock instence = new SynchronizedObjectLock();
@Override
public void run() {
// 同步代码块形式——锁为this,两个线程使用的锁是一样的,线程1必须要等到线程0释放了该锁后,才能执行
synchronized (this) {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instence);
Thread t2 = new Thread(instence);
t1.start();
t2.start();
}
}
结果:
实例代码2:
public class SynchronizedObjectLock implements Runnable{
static SynchronizedObjectLock instence = new SynchronizedObjectLock();
// 创建2把锁
Object block1 = new Object();
Object block2 = new Object();
@Override
public void run() {
// 这个代码块使用的是第一把锁,当他释放后,后面的代码块由于使用的是第二把锁,因此可以马上执行
synchronized (block1) {
System.out.println("block1锁,我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("block1锁,"+Thread.currentThread().getName() + "结束");
}
synchronized (block2) {
System.out.println("block2锁,我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("block2锁,"+Thread.currentThread().getName() + "结束");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instence);
Thread t2 = new Thread(instence);
t1.start();
t2.start();
}
}
结果:
synchronized修饰普通方法,锁对象默认为this
public class SynchronizedObjectLock implements Runnable {
static SynchronizedObjectLock instence = new SynchronizedObjectLock();
@Override
public void run() {
method();
}
public synchronized void method() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instence);
Thread t2 = new Thread(instence);
t1.start();
t2.start();
}
}
结果:
synchronized修饰静态方法
实例代码:
public class SynchronizedObjectLock implements Runnable {
static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();
@Override
public void run() {
method();
}
// synchronized用在普通方法上,默认的锁就是this,当前实例
public synchronized void method() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
public static void main(String[] args) {
// t1和t2对应的this是两个不同的实例,所以代码不会串行
Thread t1 = new Thread(instence1);
Thread t2 = new Thread(instence2);
t1.start();
t2.start();
}
}
结果:
实例代码2:
public class SynchronizedObjectLock implements Runnable {
static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();
@Override
public void run() {
method();
}
// synchronized用在静态方法上,默认的锁就是当前所在的Class类,所以无论是哪个线程访问它,需要的锁都只有一把
public static synchronized void method() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
public static void main(String[] args) {
// t1和t2对应的this是两个不同的实例,所以代码不会串行
Thread t1 = new Thread(instence1);
Thread t2 = new Thread(instence2);
t1.start();
t2.start();
}
}
结果:
synchronized修饰实例
实例代码:
public class SynchronizedObjectLock implements Runnable {
static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();
@Override
public void run() {
// 所有线程需要的锁都是同一把
synchronized(SynchronizedObjectLock.class){
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instence1);
Thread t2 = new Thread(instence2);
t1.start();
t2.start();
}
}
结果:
synchronized 底层语义原理
synchronized 锁机制在 Java 虚拟机中的同步是基于进入和退出监视器锁对象 monitor 实现的(无论是显示同步还是隐式同步都是如此),每个对象的对象头都关联着一个 monitor 对象,当一个 monitor 被某个线程持有后,它便处于锁定状态。在 HotSpot 虚拟机中,monitor 是由 ObjectMonitor 实现的,每个等待锁的线程都会被封装成 ObjectWaiter 对象,ObjectMonitor 中有两个集合,_WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表 ,owner 区域指向持有 ObjectMonitor 对象的线程。当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合尝试获取 moniter,当线程获取到对象的 monitor 后进入 _Owner 区域并把 _owner 变量设置为当前线程,同时 monitor 中的计数器 count 加1;若线程调用 wait() 方法,将释放当前持有的 monitor,count自减1,owner 变量恢复为 null,同时该线程进入 _WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放 monitor 并复位变量的值,以便其他线程获取 monitor。
_EntryList:存储处于 Blocked 状态的 ObjectWaiter 对象列表。 _WaitSet:存储处于 wait 状态的 ObjectWaiter 对象列表。
Synchronized原理分析
首先来看一个例子,深入JVM看字节码,创建如下的代码:
public class SynchTestDemo {
public static void printlnTest(){
synchronized (SynchTestDemo.class){
System.out.println("hhhhh");
}
}
public static void main(String[] args) {
printlnTest();
}
}
使用javac命令进行编译生成.class文件:
javac SynchTestDemo.java
使用javap命令反编译查看.class文件的信息
javap -verbose SynchTestDemo.class
执行完毕可以得到如下信息,如图所示:
请红色方框里的
monitorenter和monitorexit。其中monitorenter指令执行;了一次,而monitorexit指令实际上是执行了两次,第一次是正常情况下释放锁,第二次为发生异常情况时释放锁,这样做的目的在于保证线程不死锁。
monitor指令
Monitorenter和Monitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得。
monitorenter指令
在JVM规范中有提到对monitorenter指令的描述: 任何一个对象都有一个monitor与其相关联,当且有一个monitor被持有后,它将处于锁定的状态,其他线程无法来获取该monitor。当JVM执行某个线程的某个方法内部的monitorenter时,他会尝试去获取当前对应的monitor的所有权。
一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:
- monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
- 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加。
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit指令
在JVM规范中同样有提到对monitorenter指令的描述:能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程;执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
字节码分析
synchronized关键字被编译成字节码后会被翻译成monitorenter和monitorexit两条指令分别在同步块逻辑代码的起始位置与结束位置,如下图所示:
每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:
synchronized的实现原理:synchronized的底层实际是通过一个monitor对象来实现的,其实wait/notify方法也是依赖于monitor对象来实现的,这就是为什么只有在同步代码块或者方法中才能调用该方法,否则就会抛出出java.lang.IllegalMonitorStateException的异常的原因。再看一个例子:
public class SynchTestDemo {
public synchronized void printlnTest(){
System.out.println("hhhhh");
}
public static void main(String[] args) {
new SynchTestDemo().printlnTest();
}
}
根据上面的方法编译运行然后反编译获取字节码文件,可得下面的内容:
从字节码反编译的可以看出,同步方法并没有通过指令monitorenter和monitorexit来实现的,但是相对于普通方法来说,其常量池多了了 ACC_SYNCHRONIZED 标示符。JVM实际就是根据该标识符来实现方法的同步的。
当方法被调用时,会检查ACC_SYNCHRONIZED标志是否被设置,若被设置,线程会先获取monitor,获取成功才能执行方法体,方法执行完成后会再次释放monitor。在方法执行期间,其他线程都无法获得同一个monitor对象。
其实两种同步方式从本质上看是没有区别的,两个指令的执行都是JVM调用操作系统的互斥原语mutex来实现的,被阻塞的线程会被挂起、等待重新调度,会导致线程在“用户态”和“内核态”进行切换,就会对性能有很大的影响。
monitor详解
monitor通常被描述为一个对象,可以将其理解为一个同步工具,或者可以理解为一种同步机制。所有的Java对象自打new出来的时候就自带了一把锁,就是monitor锁,也就是对象锁,存在于对象头(Mark Word),锁标识位为10,指针指向的是monitor对象起始地址。
在Java虚拟机(HotSpot)中,Monitor是由其底层实际是由C++对象ObjectMonitor实现的:
ObjectMonitor() {
_header = NULL;
_count = 0; //用来记录该线程获取锁的次数
_waiters = 0,
_recursions = 0; // 线程的重入次数
_object = NULL; // 存储该monitor的对象
_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;
}
- _owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的;
- _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。cxq是一个临界资源,JVM通过CAS原子指令来修改cxq队列。修改前cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的stack(栈);
- _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中;
- _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。
举个例子具体分析一下_cxq队列与_EntryList队列的区别:
public void print() throws InterruptedException {
synchronized (obj) {
System.out.println("Hello World");
//obj.wait();
}
}
若多线程执行上面这段代码,刚开始t1线程第一次进同步代码块,能够获得锁,之后马上又有一个t2线程也准备执行这段代码,t2线程是没有抢到锁的,t2这个线程就会进入_cxq这个队列进行等待,此时又有一个线程t3准备执行这段代码,t3当然也会没有抢到这个锁,那么t3也就会进入_cxq进行等待。
接着,t1线程执行完同步代码块把锁释放了,这个时候锁是有可能被t1、t2、t3中的任何一个线程抢到的。
假如此时又被t1线程给抢到了,那么上次已经进入_cxq这个队列进行等待的线程t2、t3就会进入_EntryList进行等待,若此时来了个t4线程,t4线程没有抢到锁资源后,还是会先进入_cxq进行等待。
具体分析一下_WaitSet队列与_EntryList队列:(图片来源微信公众号 - 得物技术:精选文章|深入理解synchronzied底层原理 )
每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象。ObjectWaiter 对象里存放thread(线程对象) 和unpark的线程, 每一个等待锁的线程都会有一个ObjectWaiter对象,而objectwaiter是个双向链表结构的对象。
结合上图monitor的结构图可以分析出,当线程的拥有者执行完线程后,会释放锁,此时有可能是阻塞状态的线程去抢到锁,也有可能是处于等待状态的线程被唤醒抢到了锁。在JVM中每个等待锁的线程都会被封装成ObjectMonitor对象,_owner标识拥有该monitor的线程,而_EntryList和_WaitSet就是用来保存ObjectWaiter对象列表的,_EntryList和_WaitSet最大的区别在于前者是用来存放等待锁block状态的线程,后者是用来存放处于wait状态的线程。
当多个线程同时访问同一段代码时:
- 首先会进入_EntryList集合每当线程获取到对象的monitor后,会将monitor中的_ower变成设置为当前线程,同时会将monitor中的计数器_count加1。
- 若线程调用wait()方法时,将释放当前持有的monitor对象,将_ower设置为null,_count减1,同时该线程进入_WaitSet中等待被唤醒。
- 若当前线程执行完毕,也将释放monitor锁,并将_count值复原,以便于其他线程获取锁。
monitor对象存在于每个Java对象的对象头(Mark Word)中,所以Java中任何对象都可以作为锁,由于notify/notifyAll/wait等方法会使用到monitor锁对象,所以必须在同步代码块中使用。
多线程情况下,线程需要同时访问临界资源,监视器monitor可以确保共享数据在同一时刻只会有一个线程在访问。
可重入原理:加锁次数计数器
可重入:(来源于维基百科)若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
public class SynchronizedDemo {
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
demo.method1();
}
private synchronized void method1() {
System.out.println(Thread.currentThread().getId() + ": method1()");
method2();
}
private synchronized void method2() {
System.out.println(Thread.currentThread().getId()+ ": method2()");
method3();
}
private synchronized void method3() {
System.out.println(Thread.currentThread().getId()+ ": method3()");
}
}
执行monitorenter获取锁
- (monitor计数器=0,可获取锁)
- 执行method1()方法,monitor计数器+1 -> 1 (获取到锁)
- 执行method2()方法,monitor计数器+1 -> 2
- 执行method3()方法,monitor计数器+1 -> 3
执行monitorexit命令
- method3()方法执行完,monitor计数器-1 -> 2
- method2()方法执行完,monitor计数器-1 -> 1
- method2()方法执行完,monitor计数器-1 -> 0 (释放了锁)
- (monitor计数器=0,锁被释放了)
synchronized锁的优化
操作系统分为“用户空间”和“内核空间”,JVM是运行在“用户态”的,jdk1.6之前,在使用synchronized锁时需要调用底层的操作系统实现,其底层monitor会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从“用户态”转为“内核态”,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,这些操作给系统的并发性能 带来了很大的压力。
同这个时候CPU就需要从“用户态”切向“内核态”,在这个过程中就非常损耗性能而且效率非常低,所以说jdk1.6之前的synchronized是重量级锁。如下图所示:
简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用Mutex Lock那么将严重的影响程序的性能。不过在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。
- 锁粗化(Lock Coarsening):也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。
- 锁消除(Lock Elimination):通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本的Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。
- 轻量级锁(Lightweight Locking):这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒。
- 偏向锁(Biased Locking):是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟。
- 适应性自旋(Adaptive Spinning):当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态。
无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其底层是通过CAS实现的。无锁无法全方位代替有锁,但无锁在某些场合下的性能是非常高的。
偏向锁(无锁 -> 偏向锁)
偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及 ThreadID即可。
一开始无锁状态,JVM会默认开启“匿名”偏向的一个状态,就是一开始线程还未持有锁的时候,就预先设置一个匿名偏向锁,等一个线程持有锁之后,就会利用CAS操作将线程ID设置到对象的mark word 的高23位上【32位虚拟机】,下次线程若再次争抢锁资源的时,多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,只需要在置换ThreadID的时候依赖一次CAS原子指令即可。如图所示:
偏向锁的获取过程: 首先线程访问同步代码块,会通过检查对象头 Mark Word 的锁标志位判断目前锁的状态,如果是 01,说明就是无锁或者偏向锁,然后再根据是否偏向锁 的标示判断是无锁还是偏向锁,如果是无锁情况下,执行下一步。
线程使用 CAS 操作来尝试对对象加锁,如果使用 CAS 替换 ThreadID 成功,就说明是第一次上锁,那么当前线程就会获得对象的偏向锁,此时会在对象头的 Mark Word 中记录当前线程 ID 和获取锁的时间 epoch 等信息,然后执行同步代码块。
全局安全点(Safe Point):全局安全点的理解会涉及到 C 语言底层的一些知识,这里简单理解 SafePoint 是 Java代码中的一个线程可能暂停执行的位置。
等到下一次线程在进入和退出同步代码块时就不需要进行 CAS 操作进行加锁和解锁,只需要简单判断一下对象头的 Mark Word 中是否存储着指向当前线程的线程ID,判断的标志当然是根据锁的标志位来判断的。如果用流程图来表示的话就是下面这样:
关闭偏向锁:
偏向锁在Java 6 和Java 7 里是默认启用的。由于偏向锁是为了在只有一个线程执行同步块时提高性能,如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
关于 epoch 偏向锁的对象头中有一个被称为 epoch 的值,它作为偏差有效性的时间戳。
轻量级锁(偏向锁 -> 轻量锁)
当线程交替执行同步代码块时,且竞争不激烈的情况下,偏向锁就会升级为轻量级锁。在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。
其目标就是在只有一个线程执行同步代码块时能够提高性能。当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。
在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。
撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
轻量级锁是指当前锁是偏向锁的时候,资源被另外的线程所访问,那么偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能,下面是详细的获取过程。
轻量级锁加锁过程:
- 紧接着上一步,如果 CAS 操作替换 ThreadID 没有获取成功,执行下一步
- 如果使用 CAS 操作替换 ThreadID 失败(这时候就切换到另外一个线程的角度)说明该资源已被同步访问过,这时候就会执行锁的撤销操作,撤销偏向锁,然后等原持有偏向锁的线程到达全局安全点(SafePoint)时,会暂停原持有偏向锁的线程,然后会检查原持有偏向锁的状态,如果已经退出同步,就会唤醒持有偏向锁的线程,执行下一步
- 检查对象头中的 Mark Word 记录的是否是当前线程 ID,如果是,执行同步代码,如果不是,执行偏向锁获取流程 的第2步。
自旋锁
在很多场景下,共享资源的锁定状态只会持续很短的一段时间,为了这段时间阻塞和唤醒线程并不值得。
如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。
为了让线程等待,我们只需让线程执行一个忙循环(自旋) , 这就是自旋锁。
当一个线程t1、t2同事争抢同一把锁时,假如t1线程先抢到锁,锁不会立马升级成重量级锁,此时t2线程会自旋几次(默认自旋次数是10次,可以使用参数-XX : PreBlockSpin来更改),若t2自旋超过了最大自旋次数,那么t2就会当使用传统的方式去挂起线程了,锁也升级为重量级锁了。
自旋的等待不能代替阻塞,暂且不说对处理器数量的要求必须要两个核,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,所以如果锁被占用的时间很短,自旋等待的效果就会非常好,如果锁被占用的时间很长,那自旋的线程只会消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。
自旋锁在jdk1.4中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在jdk1.6之后自旋锁就已经默认是打开状态了。
自适应自旋锁
在JDK 1.6中引入了自适应自旋锁。这就意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋 时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间。比如增加到100此循环。相反,如果对于某个锁,自旋很少成功获取锁。那再以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,JVM对程序的锁的状态预测会越来越准确,JVM也会越来越聪明。
重量级锁
升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
重量级锁的获取流程:
- 接着上面偏向锁的获取过程,由偏向锁升级为轻量级锁,执行下一步。
- 会在原持有偏向锁的线程的栈中分配锁记录,将对象头中的 Mark Word 拷贝到原持有偏向锁线程的记录中,然后原持有偏向锁的线程获得轻量级锁,然后唤醒原持有偏向锁的线程,从安全点处继续执行,执行完毕后,执行下一步,当前线程执行第4步
- 执行完毕后,开始轻量级解锁操作,解锁需要判断两个条件:1、拷贝在当前线程锁记录的 Mark Word 信息是否与对象头中的 Mark Word 一致。2、判断对象头中的 Mark Word 中锁记录指针是否指向当前栈中记录的指针。
- 在当前线程的栈中分配锁记录,拷贝对象头中的 MarkWord 到当前线程的锁记录中,执行 CAS 加锁操作,会把对象头 Mark Word 中锁记录指针指向当前线程锁记录,如果成功,获取轻量级锁,执行同步代码,然后执行第3步,如果不成功,执行下一步。
- 当前线程没有使用 CAS 成功获取锁,就会自旋一会儿,再次尝试获取,如果在多次自旋到达上限后还没有获取到锁,那么轻量级锁就会升级为 重量级锁
使用流程图表示,如下图所示:
锁消除
锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
public class SynchRemoveDemo {
public static void main(String[] args) {
stringContact("AA", "BB", "CC");
}
public static String stringContact(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
return sb.append(s1).append(s2).append(s3).toString();
}
}
//StringBuffer 源代码中append()方法源码
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
JVM字节码分析:
StringBuffer的append()是一个同步方法,锁就是this也就是sb对象。虚拟机发现它的动态作用域被限制在stringContact()方法内部。
也就是说, sb对象的引用永远不会“逃逸”到stringContact()方法之外,其他线程无法访问到它,因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。
“对象的逃逸分析”:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象优先在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
锁粗化
JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。 可以通过下面的例子来看一下:
public class SynchDemo {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 50; i++) {
sb.append("AA");
}
System.out.println(sb.toString());
}
}
//StringBuffer 源代码中append()方法源码
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
JVM字节码代码分析:
StringBuffer的append()是一个同步方法,通过上面的代码可以看出,每次循环都要给append()方法加锁,这时系统会通过判断将其修改为下面这种,直接将原append()方法的synchronized的锁给去掉直接加在了for循环外。
public class SynchDemo {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
synchronized(sb){
for (int i = 0; i < 50; i++) {
sb.append("AA");
}
}
System.out.println(sb.toString());
}
}
//StringBuffer 源代码中append()方法源码
@Override
public StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
锁的优缺点对比
通过对象头分析锁升级过程
可以通过对象头分析工具观察一下锁升级时对象头的变化:运行时对象头锁状态分析工具JOL,是OpenJDK开源工具包,引入下方maven依赖:
<dependencies>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
</dependencies>
观察无锁状态下的对象头【无锁状态】:
import org.openjdk.jol.info.ClassLayout;
public class SynchronizedTest01 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
结果:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
这里先详细解释一下打印结果,后面就不做详细分析了:
- OFFSET : 内存地址偏移量
- SIZE : 本条信息对应的字节大小
- Instance size: 16 bytes :本次new出的Object对象的大小
由于当前所使用的的机器是64位操作系统的机器,所以前两行代表的就是对象头MarkWord,已经在上述运行结果中标出,刚好是8字节,每个字节8位,刚好是64位;由前文中32位对象头与64位对象头的位数对比可知,分析对象头锁升级情况看第一行的对象头即可。
第三行指的是类型指针(上文中有说过,指向的是方法区的类元信息),已经在上述运行结果中标出,Klass Pointer在64位机器默认是8字节,这里由于指针压缩的原因当前是4字节。
第四行指的是对齐填充,有的时候有有的时候没有,JVM内部需要保证对象大小是8个字节的整数倍,实际上计算机底层通过大量计算得出对象时8字节的整数倍可以提高对象存储的效率。
可以观察到本次new出的Object对象的大小实际只有12字节,这里对象填充为其填充了4个字节,就是为了让Object对象大小为16字节是8字节的整数倍。
JVM采用的是小端模式,需要现将其转换成大端模式,具体转换如下图所示:
可以看出一开始对象没有加锁,通过最后三位的“001”也能观察到,前25位代表hashcode,那这里为什么前25位是0呢?其实hashcode是通过C语言类似于“懒加载”的方式获取到的,所以看到该对象的高25位并没有hashcode。
观察有锁无竞争状态下的对象头【无锁->偏向锁】:
import org.openjdk.jol.info.ClassLayout;
public class SynchronizedTest02 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
}
运行结果(JVM默认小端模式):
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 90 39 62 05 (10010000 00111001 01100010 00000101) (90323344)
4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
运行结果分析:
通过运行结果可以看到,先打印出来的是一个“001”无锁的状态,但是后打印出来的“000”并不是偏向锁的状态,查上面的表可以发现“000”直接就是轻量级锁的状态了。
JVM启动的时候内部实际上也是有很多个线程在执行synchronized,JVM就是为了避免无畏的锁升级过程(偏向锁->轻量级锁->重量级锁)带来的性能开销,所以JVM默认状态下会延迟启动偏向锁。
只要将代码前面加个延迟时间即可观察到偏向锁:
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
public class SynchronizedTest02 {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(6);
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
运行结果(JVM默认小端模式):
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 80 de (00000101 10010000 10000000 11011110) (-561999867)
4 4 (object header) b2 7f 00 00 (10110010 01111111 00000000 00000000) (32690)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
对未开启偏向锁与开启偏向锁的运行结果分析:
- 未开启偏向锁(大端模式),没加锁:00000000 00000000 00000000 00000001
- 开启偏向锁(大端模式),没加锁 :00000000 00000000 00000000 00000101
- 开启偏向锁(大端模式),加锁 :11011110 10000000 10010000 00000101
开启偏向锁之后的无锁状态,会加上一个偏向锁,叫匿名偏向(可偏向状态),表示该对象锁是可以加偏向锁的,从高23位的23个0可以看出暂时还没有偏向任何一个线程,代表已经做好了偏向的准备,就等着接下来的某个线程能拿到就直接利用CAS操作把线程id记录在高23位的位置。
观察有锁有竞争状态下的对象头【偏向锁->轻量级锁】:
import org.openjdk.jol.info.ClassLayout;
public class SynchronizedTest03 {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
Object object = new Object();
//main线程
System.out.println(ClassLayout.parseInstance(object).toPrintable());
//线程t1
new Thread(() -> {
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
},"t1").start();
Thread.sleep(2000);
//main线程
System.out.println(ClassLayout.parseInstance(object).toPrintable());
//线程t2
new Thread(() -> {
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
},"t2").start();
}
}
运行结果(JVM默认小端模式):
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) //main线程打印
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 94 2d (00000101 10010000 10010100 00101101) (764710917) //t1线程打印
4 4 (object header) c9 7f 00 00 (11001001 01111111 00000000 00000000) (32713)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 94 2d (00000101 10010000 10010100 00101101) (764710917) //main线程打印
4 4 (object header) c9 7f 00 00 (11001001 01111111 00000000 00000000) (32713)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 08 a9 d5 07 (00001000 10101001 11010101 00000111) (131442952) //t2线程打印
4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
运行结果分析: 一开始main线程打印出的object对象头可以看出是匿名偏向;
接着线程t1打印了object对象头,可以与第一个打印出来的对象头对比不难发现t1打印的也是偏向锁,但是t1打印的对象头已经把t1的线程id记录在了其对应的23位;
程序再次回到main线程,其还是打印出来刚刚t1的对象头数据,也就是说偏向锁一旦偏向了某个线程后,如果线程不能重新偏向的话,那么这个偏向锁还是会一直记录着之前偏向的那个线程的对象头状态;
接着线程t2又开始打印了object对象头,可以看出最后一次打印已经升级成了轻量级锁,因为这里已经存在两个线程t1、t2交替进入了object对象锁的同步代码块,并且锁的不激烈竞争,所以锁已经升级成了轻量级锁。
观察无锁升级成重量级锁状态下的对象头的整个过程【无锁->重量级锁】:
import org.openjdk.jol.info.ClassLayout;
public class SynchronizedTest04 {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
new Thread(()->{
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
//延长锁的释放,造成锁的竞争
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t0").start();
Thread.sleep(5000);
new Thread(() -> {
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
//延长锁的释放,造成锁的竞争
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1").start();
new Thread(() -> {
synchronized (object) {
System.out.println(ClassLayout.parseInstance(object).toPrintable());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t2").start();
}
}
运行结果:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) //main线程打印
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 d8 8f ef (00000101 11011000 10001111 11101111) (-275785723) //t0线程打印
4 4 (object header) ce 7f 00 00 (11001110 01111111 00000000 00000000) (32718)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 00 e9 a9 09 (00000000 11101001 10101001 00001001) (162130176) //t1线程打印
4 4 (object header) ce 7f 00 00 (11001110 01111111 00000000 00000000) (32718)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a d8 80 f0 (00001010 11011000 10000000 11110000) (-259991542) //t2线程打印
4 4 (object header) ce 7f 00 00 (11001110 01111111 00000000 00000000) (32718)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
运行结果分析(JVM默认小端模式): 程序一开始就是设置了5秒钟的睡眠,目的在于让JVM优先加载完成后,让JVM默认状态下会延迟启动偏向锁,可以开出一开始main线程打印的是“101”就是默认的匿名偏向锁,但是并没有设置线程id;之后t0线程就立马打印了,此时只需利用CAS操作把t0的线程id设置进对象头即可,所以这个时候也是一个偏向锁状态;之后的程序睡眠5秒钟后,程序中t1、t2线程执行代码块时,有意的将其线程睡眠几秒钟,目的在于不管那个线程率先抢到锁,都能让另外一个线程在自旋等待中,所以t1线程打印的是“00”就已经是轻量级锁了,最后看程序执行结果,t2打印的是“10”就已经升级为重量级锁了,显然t2线程已经超过了自旋的最大次数,已经转成重量级锁了。
参考文章
- www.pdai.tech/md/java/thr…
- 微信公众号(得物技术) :精选文章|深入理解synchronzied底层原理
- blog.csdn.net/a745233700/…
- 《深入理解Java虚拟机》+《Java并发编程的艺术》
- juejin.im/post/5ae6dc…
- www.cnblogs.com/javaminer/p…
- www.jianshu.com/p/dab7745c0…
- www.cnblogs.com/wuchaodzxx/…
- www.cnblogs.com/xyabk/p/109…
- www.jianshu.com/p/64240319e…