目录java
3、锁的优化数据结构
一、锁升级多线程
二、锁粗化ide
三、锁消除布局
Synchronized是一个同步关键字,在某些多线程场景下,若是不进行同步会致使数据不安全,而Synchronized关键字就是用于代码同步。什么状况下会数据不安全呢,要知足两个条件:一是数据共享(临界资源),二是多线程同时访问并改变该数据。性能
例如:优化
public class AccountingSync implements Runnable{ //共享资源(临界资源) static int i=0; /** * synchronized 修饰实例方法 */ public synchronized void increase(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { AccountingSync instance=new AccountingSync(); Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
该段程序的输出为:2000000this
可是若是increase的synchronized被删除,那么极可能输出结果就会小于2000000,这是由于多个线程同时访问临界资源i,若是一个线程A对i=88的自增到89没有被B线程读取到,线程B认为i仍然是88,那么线程B对i的自增结果仍是89,那么这里就会出现问题。
Synchronized锁的3种使用形式(使用场景):
注意:
一、Java对象头
首先,咱们要知道对象在内存中的布局:
已知对象是存放在堆内存中的,对象大体能够分为三个部分,分别是对象头、实例变量和填充字节。
经过第一部分能够知道,Synchronized不管是修饰方法仍是代码块,都是经过持有修饰对象的锁来实现同步,那么Synchronized锁对象是存在哪里的呢?答案是存在锁对象的对象头的MarkWord中。那么MarkWord在对象头中到底长什么样,也就是它到底存储了什么呢?
在32位的虚拟机中:
在64位的虚拟机中:
上图中的偏向锁和轻量级锁都是在java6之后对锁机制进行优化时引进的,下文的锁升级部分会具体讲解,Synchronized关键字对应的是重量级锁,接下来对重量级锁在Hotspot JVM中的实现锁讲解。
二、Synchronized在JVM中的实现原理
重量级锁对应的锁标志位是10,存储了指向重量级监视器锁的指针,在Hotspot中,对象的监视器(monitor)锁对象由ObjectMonitor对象实现(C++),其跟同步相关的数据结构以下:
ObjectMonitor() { _count = 0; //用来记录该对象被线程获取锁的次数 _waiters = 0; _recursions = 0; //锁的重入次数 _owner = NULL; //指向持有ObjectMonitor对象的线程 _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 }
光看这些数据结构对监视器锁的工做机制仍是一头雾水,那么咱们首先看一下线程在获取锁的几个状态的转换:
线程的生命周期存在5个状态,start、running、waiting、blocking和dead
对于一个synchronized修饰的方法(代码块)来讲:
那么Synchronized修饰的代码块/方法如何获取monitor对象的呢?
在JVM规范里能够看到,不论是方法同步仍是代码块同步都是基于进入和退出monitor对象来实现,然而两者在具体实现上又存在很大的区别。经过javap对class字节码文件反编译能够获得反编译后的代码。
(1)Synchronized修饰代码块:
Synchronized代码块同步在须要同步的代码块开始的位置插入monitorentry指令,在同步结束的位置或者异常出现的位置插入monitorexit指令;JVM要保证monitorentry和monitorexit都是成对出现的,任何对象都有一个monitor与之对应,当这个对象的monitor被持有之后,它将处于锁定状态。
例如,同步代码块以下:
public class SyncCodeBlock { public int i; public void syncTask(){ synchronized (this){ i++; } } }
对同步代码块编译后的class字节码文件反编译,结果以下(仅保留方法部分的反编译内容):
public void syncTask(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter //注意此处,进入同步方法 4: aload_0 5: dup 6: getfield #2 // Field i:I 9: iconst_1 10: iadd 11: putfield #2 // Field i:I 14: aload_1 15: monitorexit //注意此处,退出同步方法 16: goto 24 19: astore_2 20: aload_1 21: monitorexit //注意此处,退出同步方法 22: aload_2 23: athrow 24: return Exception table: //省略其余字节码.......
能够看出同步方法块在进入代码块时插入了monitorentry语句,在退出代码块时插入了monitorexit语句,为了保证不管是正常执行完毕(第15行)仍是异常跳出代码块(第21行)都能执行monitorexit语句,所以会出现两句monitorexit语句。
(2)Synchronized修饰方法:
Synchronized方法同步再也不是经过插入monitorentry和monitorexit指令实现,而是由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志隐式实现的,若是方法表结构(method_info Structure)中的ACC_SYNCHRONIZED标志被设置,那么线程在执行方法前会先去获取对象的monitor对象,若是获取成功则执行方法代码,执行完毕后释放monitor对象,若是monitor对象已经被其它线程获取,那么当前线程被阻塞。
同步方法代码以下:
public class SyncMethod { public int i; public synchronized void syncTask(){ i++; } }
对同步方法编译后的class字节码反编译,结果以下(仅保留方法部分的反编译内容):
public synchronized void syncTask(); descriptor: ()V //方法标识ACC_PUBLIC表明public修饰,ACC_SYNCHRONIZED指明该方法为同步方法 flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field i:I 5: iconst_1 6: iadd 7: putfield #2 // Field i:I 10: return LineNumberTable: line 12: 0 line 13: 10 }
能够看出方法开始和结束的地方都没有出现monitorentry和monitorexit指令,可是出现的ACC_SYNCHRONIZED标志位。
锁的4中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)
(1)偏向锁:
为何要引入偏向锁?
由于通过HotSpot的做者大量的研究发现,大多数时候是不存在锁竞争的,经常是一个线程屡次得到同一个锁,所以若是每次都要竞争锁会增大不少没有必要付出的代价,为了下降获取锁的代价,才引入的偏向锁。
偏向锁的升级
当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,由于偏向锁不会主动释放锁,所以之后线程1再次获取锁的时候,须要比较当前线程的threadID和Java对象头中的threadID是否一致,若是一致(仍是线程1获取锁对象),则无需使用CAS来加锁、解锁;若是不一致(其余线程,如线程2要竞争锁对象,而偏向锁不会主动释放所以仍是存储的线程1的threadID),那么须要查看Java对象头中记录的线程1是否存活,若是没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)能够竞争将其设置为偏向锁;若是存活,那么马上查找该线程(线程1)的栈帧信息,若是仍是须要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,若是线程1 再也不使用该锁对象,那么将锁对象状态设为无锁状态,从新偏向新的线程。
偏向锁的取消:
偏向锁是默认开启的,并且开始时间通常是比应用程序启动慢几秒,若是不想有这个延迟,那么可使用-XX:BiasedLockingStartUpDelay=0;
若是不想要偏向锁,那么能够经过-XX:-UseBiasedLocking = false来设置;
(2)轻量级锁
为何要引入轻量级锁?
轻量级锁考虑的是竞争锁对象的线程很少,并且线程持有锁的时间也不长的情景。由于阻塞线程须要CPU从用户态转到内核态,代价较大,若是刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,所以这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
轻量级锁何时升级为重量级锁?
线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中建立的用于存储锁记录的空间(称为DisplacedMarkWord),而后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;
若是在线程1复制对象头的同时(在线程1CAS以前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,可是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。
可是若是自旋的时间太长也不行,由于自旋是要消耗CPU的,所以自旋的次数是有限制的,好比10次或者100次,若是自旋次数到了线程1尚未释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
*注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁能够升级不能够降级,可是偏向锁状态能够被重置为无锁状态。
(3)这几种锁的优缺点(偏向锁、轻量级锁、重量级锁)
按理来讲,同步块的做用范围应该尽量小,仅在共享数据的实际做用域中才进行同步,这样作的目的是为了使须要同步的操做数量尽量缩小,缩短阻塞时间,若是存在锁竞争,那么等待锁的线程也能尽快拿到锁。
可是加锁解锁也须要消耗资源,若是存在一系列的连续加锁解锁操做,可能会致使没必要要的性能损耗。
锁粗化就是将多个连续的加锁、解锁操做链接在一块儿,扩展成一个范围更大的锁,避免频繁的加锁解锁操做。
Java虚拟机在JIT编译时(能够简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),经过对运行上下文的扫描,通过逃逸分析,去除不可能存在共享资源竞争的锁,经过这种方式消除没有必要的锁,能够节省毫无心义的请求锁时间