synchronized底层揭秘

 

前言

上篇文章咱们从硬件级别探索,对可见性和有序性的认识上升了一个高度,却迟迟没有介绍原子性的解决方案。java

今天咱们就来聊一聊原子性的解决方案,编程

引入锁机制,除了能够保证原子性,同时也能够保证可见性和有序性。并发

相信小伙伴们对于synchronized互斥锁必定很熟悉,可是你懂它的实现原理吗,今天就让咱们一块儿来揭开它的神秘面纱吧。app

 

synchronized的原子性

首先咱们来看一下synchronized是怎么保证原子性的。jvm

其实往最简单了解释,仍是比较容易理解的。synchronized加锁主要靠的是monitor,monitor在java里能够理解成一个监视器,在操做系统里它又被称为管程。布局

简单的模型以下图:优化

当咱们的程序经过synchronized锁定一个对象的时候,这个对象会关联一个monitor,获取锁时会对monitor中的计数器进行+1操做,释放锁的时候进行-1操做,同时也是支持可重入的,同一个线程再次获取该对象的锁,计数器就再+1。ui

若是计数器为0就表明彻底释放了锁,其余线程能够获取锁。this

若是线程调用了wait方法,会释放锁资源,同时把线程放入waitset中,等待notifyall方法唤醒,唤醒后从新开始竞争锁资源。spa

这就是sychronized锁的最简单的解释,咱们固然不会知足于此,接下来咱们继续深刻研究一下

先看一段代码:

MyLock lock = new MyLock();//一个自定义的锁对象
sychronized(lock){
//...
}

java的对象在内存中存储的布局能够分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头包含Mark Word(包含hashcode、锁数据、GC信息等)和Class MetaData Address(指向Class对象的指针)。

实例数据就是咱们在对象里存放的那些数据。

java要求对象大小为8字节的整数倍,对齐填充就是用来填充字节的,没有其余意义。

Mark Word会指向一个monitor,这个monitor是C++实现的一个Object Monitor对象,首先线程在获取锁时,先进入到entry list中,而后经过CAS对count计数器进行+1操做,若是+1成功表明获取到锁,此时就会把该线程的信息放入owner中,owner就是用来存储当前获取到锁的线程的。总体结构如图所示:

 

 

 

 

sychronized的可见性

在说可见性以前,咱们先引入两个概念:Store屏障和Load屏障

Store屏障就是强制CPU执行flush操做,Load屏障就是强制CPU执行refresh操做。

flush和refresh咱们上篇文章已经说过,这里就再也不解释了。

那sychronized是如何实现可见性的呢,其实就是利用了内存屏障。以下:

sychronized(this){
   // monitorenter
   // Load内存屏障
   //...  
}
//monitorexit
//Store内存屏障

 

sychronized的有序性

一样在说有序性以前引入两个新的内存屏障:Acquire屏障和Release屏障

Acquire屏障能够禁止读操做和其余读写操做之间发生指令重排,Release屏障能够禁止写操做和其余读写操做之间发生指令重排。

那sychronized是如何实现有序性的呢,其实就是利用了这两个内存屏障。以下:

sychronized(this){
   // monitorenter
   // Load内存屏障
   // Acquire内存屏障
   //...  
   //Release内存屏障
}
 //monitorexit
 //Store内存屏障

须要注意的是Acquire屏障和Release屏障保证的是sychronized内部的代码不会与外部的代码之间发生指令重排,内部的代码本身仍是可能发生指令重排的。

 

sychronized的锁优化

jdk1.6后jvm对sychronized进行了锁优化,这部分咱们作个概念了解就能够了。

1.锁消除

锁消除是JIT编译器对sychronized的优化,在编译的时候会经过逃逸分析技术,来分析锁对象。若是只有一个线程来加锁和解锁,没有锁竞争,那就没有必要加锁,会去掉monitorenter和monitorexit指令。

2.锁粗化

这个意思是,若是有多个连续的加锁释放锁操做,那么编译后会变成一把锁。

例如

sychronized(this){}

sychronized(this){}

sychronized(this){}

连着三个加锁操做,编译后会变成一个。

3.偏向锁

偏向锁主要是为了减小monitorenter和monitorexit指令的CAS操做,减小开销,若是认为当前锁大几率只有一个线程来竞争,那么就会给这个锁维护好一个偏好Bias,以后该线程加锁和释放锁都经过这个Bias来执行,不须要去执行CAS了。

可是若是发现有其余线程来竞争锁,就会收回以前分配好的偏好。

4.轻量级锁

若是偏向锁没能实现,也就是说有多个线程竞争锁,那么就会采用轻量级锁。

其实就是将对象里的轻量级锁指针指向一个已经获取了锁的线程,而后判断一下是否是本身加的锁,若是是就直接执行,若是不是说明有其余线程加了锁,就会升级为重量级锁,重量级锁流程咱们上文中介绍原子性的时候已经说过了。

5.适应性自旋锁

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复的花费可能会让系统得不偿失。为了让当前线程“稍等一下”,咱们需让当前线程进行自旋。

若是在自旋完成后前面锁同步资源的线程已经释放了锁,那么当前线程就能够没必要阻塞而是直接获取同步资源,从而避免了切换线程的开销。这就是自旋锁。

适应性自旋锁意味着自旋的时间(次数)再也不固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

 

总结

到这里,有关synchronized的底层实现咱们基本上已经聊完了。

使用锁来保证原子性,使用内存屏障来保证可见性和有序性。

同时jvm又对sychronized作了一些优化。

相信小伙伴们理解了本文的内容,会收获颇丰。

那咱们下次再见。

 

往期文章推荐:

JVM专栏

消息中间件专栏

并发编程专栏

相关文章
相关标签/搜索