前言

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

今天我们就来聊一聊原子性的解决方案,锁。

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

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

synchronized的原子性

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

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

简单的模型如下图:

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

如果计数器为0就代表完全释放了锁,其他线程可以获取锁。

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

这就是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专栏

消息中间件专栏

并发编程专栏

synchronized底层揭秘的更多相关文章

  1. synchronized底层实现学习

    上文我们总结了 synchronized 关键字的基本用法以及作用,并未涉及 synchronized 底层是如何实现的,所谓刨根问底,本文我们就开始 synchronized 原理的探索之旅吧(*& ...

  2. Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  3. 并发-Synchronized底层优化(偏向锁、轻量级锁)

    Synchronized底层优化(偏向锁.轻量级锁) 参考: http://www.cnblogs.com/paddix/p/5405678.html 一.重量级锁 上篇文章中向大家介绍了Synchr ...

  4. 面试官都叫好的Synchronized底层实现,这工资开多少一个月?

    本文为死磕Synchronized底层实现第三篇文章,内容为重量级锁实现. 本系列文章将对HotSpot的synchronized锁实现进行全面分析,内容包括偏向锁.轻量级锁.重量级锁的加锁.解锁.锁 ...

  5. 一文让你读懂Synchronized底层实现,秒杀面试官

    本文为死磕Synchronized底层实现第三篇文章,内容为轻量级锁实现. 轻量级锁并不复杂,其中很多内容在偏向锁一文中已提及过,与本文内容会有部分重叠. 另外轻量级锁的背景和基本流程在概论中已有讲解 ...

  6. 说一下 synchronized 底层实现原理?(未完成)

    说一下 synchronized 底层实现原理?(未完成)

  7. Java多线程和并发(八),synchronized底层原理

    目录 1.对象头(Mark Word) 2.对象自带的锁(Monitor) 3.自旋锁和自适应自旋锁 4.偏向锁 5.轻量级锁 6.偏向锁,轻量级锁,重量级锁联系 八.synchronized底层原理 ...

  8. 死磕synchronized底层实现

    点赞再看,养成习惯,微信搜索[三太子敖丙]第一时间阅读. 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点.资料以及我的系列文章. 前言 ...

  9. synchronized底层是怎么实现的?

    前言 面试的时候有被问到,synchronized底层是怎么实现的,回答的比较浅,面试官也不是太满意,所以觉得要好好总结一下,啃啃这个硬骨头. synchronized使用场景 我们在使用synchr ...

随机推荐

  1. 从原生web组件到框架组件源码(三)

    快乐的时光都是这么短暂,转眼间,web原生组件的知识点已经学完了,这个虽然暂时不一定有用,但是随着时间的积累,一步一个脚印的积累,你会有相应的收获,希望能变得更强,比如两年前我也会想有现成的东西不用, ...

  2. Flask中的RESTFul

    RESTFul 1.什么是RESTFul? 1.1 简介 REST即表述性状态传递(英文:Representational State Transfer, 简称REST)是Roy Fielding博士 ...

  3. AJAX接口-拉购网职位搜索爬虫

    拉购网职位搜索爬虫 分析职位搜索调用接口: 浏览器开发者模式(快捷键F12)切换手机模式,打开拉购网职位搜索链接 https://m.lagou.com/search.html 输入搜索关键词, 例如 ...

  4. 鸿蒙开发板外设控制 之 实现按键“按下事件”和“释放事件”的通用框架(V0.0.1)

    在帖子 <鸿蒙开发板外设控制>直播图文版(2020.10.28) 中我们提到过:"开发板上的按键也可以看作一种 GPIO 外设." 因此,要捕捉按键的状态(按下或释放) ...

  5. 深入浅出 webpack 之基础配置篇

    前言 前端工程化经历过很多优秀的工具,例如 Grunt.Gulp.webpack.rollup 等等,每种工具都有自己适用的场景,而现今应用最为广泛的当属 webpack 打包了,因此学习好 webp ...

  6. EMP-面向未来微前端方案正式开源了!

    原文团队掘金平台:https://juejin.im/post/6891532248269783054 EMP项目github链接: https://github.com/efoxTeam/emp E ...

  7. 315. Count of Smaller Numbers After Self(二分或者算法导论中的归并求逆序数对)

    You are given an integer array nums and you have to return a new counts array. The counts array has ...

  8. 1. 线性DP 152. 乘积最大子数组

    152. 乘积最大子数组  https://leetcode-cn.com/problems/maximum-product-subarray/ func maxProduct(nums []int) ...

  9. 【JVM】肝了一周,吐血整理出这份超硬核的JVM笔记(升级版)!!

    写在前面 最近,一直有小伙伴让我整理下关于JVM的知识,经过十几天的收集与整理,初版算是整理出来了.希望对大家有所帮助. JDK 是什么? JDK 是用于支持 Java 程序开发的最小环境. Java ...

  10. Java 类型信息详解和反射机制

    本文部分摘自 On Java 8 RTTI RTTI(RunTime Type Information)运行时类型信息,能够在程序运行时发现和使用类型信息,把我们从只能在编译期知晓类型信息并操作的局限 ...