【搞定面试官】- Synchronized如何实现同步?锁优化?(1)
前言
说起Java面试中最高频的知识点非多线程莫属。每每提起多线程都绕不过一个Java关键字——synchronized。我们都知道该关键字可以保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块以保证多线程的安全性。那么,本篇文章我们就来揭开这个synchronized的面纱。
线程安全的实现方法
在详细介绍synchronized之前,我们首先了解一下实现线程安全的不同方式,了解synchronized是如何实现线程安全的理论基础,做到心中有数。目前主要有三种线程安全实现方法:互斥同步(阻塞同步)、非阻塞同步以及无需同步的线程安全方案。
- 互斥同步(Mutual Exclusion & Synchnronization)
互斥同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此在互斥同步四个字中,互斥是因,同步是果;互斥是方法,同步是目的。
Java中最基本的互斥同步手段就是synchronized,具体如何实现的互斥同步请继续往下看。
btw,除了synchronized,还有另外一种实现同步的方式,那就是java.util.concurrent包中的重入锁ReentrantLock,具体细节就不细说了,它和synchronized用法几乎一样。只是synchronized是原生语法,而ReentrantLock是JDK提供的API层面的互斥锁。
- 非阻塞同步
互斥同步主要同步阻塞线程来保证线程安全,因此也被称为阻塞同步。它认为只要不去做正确的同步方式(例如加锁),那就一定会出现问题,无论共享数据是否会出现竞争(悲观锁)。
回来随着硬件指令集的发展,我们有了另外一种选择:先进行操作,如果没有其他线程争用,那操作就成功了;如果有其他线程争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要把线程挂起,所以这种同步方式成为非阻塞同步。
- 无需同步的线程安全方案
要保证线程安全,并不一定就要进行同步,两者并没有因果关系。如果一个方法本来就不涉及共享数据,那它自然无需任何同步手段去保证正确性,因此会有一些代码天生线程安全。比如可重入代码(Reentrant Code)和线程本地存储(Thread Local Storage)等。
JDK中的synchronized改进
在 JDK1.5 之前,Java 是依靠 Synchronized 关键字实现锁功能来做到线程安全。Synchronized 是 JVM 实现的一种内置锁,锁的获取和释放是由 JVM 隐式实现。
到了 JDK1.5 版本,java.util.concurrent包中新增了 Lock 接口来实现锁功能,它提供了与 Synchronized 关键字类似的同步功能,只是在使用时需要显示获取和释放锁。前边我们提到过,Lock 同步锁是基于 Java 实现的,而 Synchronized 是基于底层操作系统的 Mutex Lock 实现的,每次获取和释放锁操作都会带来用户态和内核态的切换,从而增加系统性能开销。因此,在锁竞争激烈的情况下,Synchronized 同步锁在性能上就表现得非常糟糕,它也常被大家称为重量级锁。特别是在单个线程重复申请锁的情况下,JDK1.5 版本的 Synchronized 锁性能要比 Lock 的性能差很多。例如,在 Dubbo 基于 Netty 实现的通信中,消费端向服务端通信之后,由于接收返回消息是异步,所以需要一个线程轮询监听返回信息。而在接收消息时,就需要用到锁来确保 request session 的原子性。如果我们这里使用 Synchronized 同步锁,那么每当同一个线程请求锁资源时,都会发生一次用户态和内核态的切换。
到了 JDK1.6 版本之后,Java 对 Synchronized 同步锁做了充分的优化,甚至在某些场景下,它的性能已经超越了 Lock 同步锁。
synchronized使用方式
Java中万物皆对象,而每一个对象都可以加锁,这是synchronized保证线程安全的基础。
对于同步方法,锁是当前实例对象,即
this,对该类其他实例对象无影响。对于静态同步方法,锁是当前对象的 Class 对象, 影响其他该类的实例化对象。
对于同步方法块,锁是
synchronized括号里配置的对象。
也就是说,我们可以利用synchronized修饰类,类中的方法或者方法块。如下面的代码,分别对应上述三种情形。
public class synchronizedTest implements Runnable {
static synchronizedTest instance=new synchronizedTest();
public void run() {
synchronized(instance){
//同步代码块,对应文章中第3点
//*******
}
}
void synchronized method1() {} //类中的同步方法 对应文章中第1点
void static synchronized method2() {} ////类中静态同步方法 对应文章中第2点
}
同步方法块
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁存在哪里呢?锁里面会存储什么信息呢?我们先来看一段代码以及它的字节码(我这里用的Idea的jclasslib插件)。
package techgo.blog;
public class SynchronizedTest {
private int i = 0;
public void fun() {
synchronized (this) {
i ++;
}
}
}

我们看到monitorenter和monitorexit,之后查阅虚拟机字节码指令表,我们知道这两个字节码操作分别表示获得和释放对象的锁。进入 monitorenter 指令后,线程将持有 Monitor 对象,退出 monitorenter 指令后,线程将释放该 Monitor 对象。以上这是同步方法块的实现方式。
同步方法
对于同步方法来说,如果去查看其字节码,我们会看不到这两个指令,因为同步方法依靠的是方法修饰符上的ACC_SYNCHRONIZED来实现的:
public synchronized void fun1() {
}

当方法调用时,调用指令将会检查该方法是否被设置 ACC_SYNCHRONIZED 访问标志。如果设置了该标志,执行线程将先持有 Monitor 对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该 Mointor 对象,当方法执行完成后,再释放该 Monitor 对象。
synchronized锁的实现
synchronized的对象锁,其指针指向的是一个monitor对象(由C++实现)的起始地址。每个对象实例都会有一个 monitor。其中monitor可以与对象一起创建、销毁;亦或者当线程试图获取对象锁时自动生成。需要注意的是monitor不是Java特有的概念,想了解更多monitor的详细介绍可以查看这篇文章。
在HotSpot虚拟机中,最终采用ObjectMonitor类实现monitor。
openjdk\hotspot\src\share\vm\runtime\objectMonitor.hpp源码如下:
ObjectMonitor() {
_count = 0;
_owner = NULL;//指向获得ObjectMonitor对象的线程或基础锁
_EntryList = NULL ;//处于等待锁block状态的线程,会被加入到entry set;
_WaitSet = NULL;//处于wait状态的线程,会被加入到wait set;
_WaitSetLock = 0 ;
_header = NULL;//markOop对象头
_waiters = 0,//等待线程数
_recursions = 0;//重入次数
_object = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
_previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
}
当多个线程同时访问一段同步代码时,多个线程会先被存放在 ContentionList 和 _EntryList 集合中,处于 block 状态的线程,都会被加入到该列表。接下来当线程获取到对象的 Monitor 时,Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex,竞争失败的线程会再次进入 ContentionList 被挂起。
如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入 WaitSet 集合中,等待下一次被唤醒。如果当前线程顺利执行完方法,也将释放 Mutex。

继续深入(锁优化)
我们都知道,对象被创建在堆中。并且对象在内存中的存储布局方式可以分为3块区域:对象头、实例数据、对齐填充。

对于对象头来说,主要是包括俩部分信息Mark Word和Klass Point:
- Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

- 另一部分是类型指针Klass Point:JVM通过这个指针来确定这个对象是哪个类的实例。
锁升级功能主要依赖于 Mark Word 中的锁标志位和释放偏向锁标志位,Synchronized 同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。好了今天就先到这了,锁优化的细节还在码字中。。
参考资料:
《深入理解Java虚拟机》 第二版
https://blog.csdn.net/wangyadong317/article/details/84065828
https://blog.csdn.net/zjy15203167987/article/details/82531772
https://www.cnblogs.com/JsonShare/p/11433302.html
https://baijiahao.baidu.com/s?id=1612142459503895416&wfr=spider&for=pc
https://www.php.cn/java-article-410323.html
本文由博客一文多发平台 OpenWrite 发布!
文章首发:https://zhuanlan.zhihu.com/lovebell
个人公众号:技术Go
您的点赞与支持是作者持续更新的最大动力!
【搞定面试官】- Synchronized如何实现同步?锁优化?(1)的更多相关文章
- 搞定面试官 - MySQL 中你知道如何计算一个索引的长度嘛?
大家好,我是程序员啊粥. 今天给大家分享一个我遇到过的比较少见的面试题,那就是 MySQL 中如何计算一个索引的长度. 说实话,我第一次遇到这个问题的时候想当然的以为索引长度就是我们建表时定义的字段长 ...
- 搞定面试官 - 你可以介绍一下在 MySQL 中,哪些情况下 索引会失效嘛?
大家好,我是程序员啊粥,前边给大家分享了 *MySQL InnoDB 索引模型 在 MySQL InnoDB 中,为什么 delete 删除数据之后表数据文件大小没有变 如何计算一个索引的长度 如何查 ...
- 金三银四,2018最新iOS面试题,由它可以搞定面试官?
序言 这些资料,你一定会用到!我相信很多人都在说,iOS行业不好了,iOS现在行情越来越难了,失业的人比找工作的人还要多.失业即相当于转行,跳槽即相当于降低自己的身价.那么做iOS开发的你,你是否在时 ...
- 【搞定面试官】try中有return,finally还会执行吗?
本篇文章我们主要探讨 一下如果try {}语句中有return,这种情况下finally语句还会执行吗?其实JVM规范是对这种情况有特殊规定的,那我就先上代码吧! public class Final ...
- 搞定面试官:咱们从头到尾再说一次 Java 垃圾回收
接着前几天的两篇文章,继续解析JVM面试问题,送给年后想要跳槽的小伙伴 万万没想到,面试中,连 ClassLoader类加载器 也能问出这么多问题..... 万万没想到,JVM内存区域的面试题也可以问 ...
- RabbitMQ:从入门到搞定面试官
安装 使用docker安装,注意要安装tag后缀为management的镜像(包含web管理插件),我这里使用的是rabbitmq:3.8-management 1. 拉取镜像 shell docke ...
- 【搞定面试官】谈谈你对JDK中Executor的理解?
## 前言 随着当今处理器计算能力愈发强大,可用的核心数量越来越多,各个应用对其实现更高吞吐量的需求的不断增长,多线程 API 变得非常流行.在此背景下,Java自JDK1.5 提供了自己的多线程框架 ...
- 【搞定面试官】你还在用Executors来创建线程池?会有什么问题呢?
前言 上文我们介绍了JDK中的线程池框架Executor.我们知道,只要需要创建线程的情况下,即使是在单线程模式下,我们也要尽量使用Executor.即: ExecutorService fixedT ...
- 搞定面试官 - 可以介绍一下在 MySQL 中你平时是怎么使用 COUNT() 的嘛?
大家好,我是程序员啊粥. 相信在大家的工作中,有很多的功能都需要用到 count(*) 来统计表中的数据行数.同时,对于一些大数据的表,用 count 都是瑟瑟发抖,往往会结合缓存等进行处理. 那么, ...
随机推荐
- POJ3252 Round Numbers 题解 数位DP
题目大意: 求区间 \([x,y]\) 范围内有多少数的二进制表示中的'0'的个数 \(\ge\) '1'的个数. 解题思路: 使用 数位DP 解决这个问题. 我们设状态 f[pos][num0][n ...
- 记: Spring Data Jpa @OneToMany 级联查询被动触发的问题
I have encountered a bug in using Spring Data Jpa. Specifically,when @OneToMany was used to maintain ...
- Python 超级玛丽代码实现:人物行走和碰撞检测
功能介绍 人物行走 人物的行走速度这边分成水平方向(X轴)和竖直方向(Y轴),水平方向的速度要考虑加速度和摩擦力,竖直方向的速度要考虑重力加速度. 水平方向:设定X轴向右走的速度为大于0,向左走的速度 ...
- 《算法笔记》之基础C/C++入门
开始进行算法笔记的学习,在此纪录下来,同时作为自己日后复习资料. 1.基本语法 #include <iostream> using namespace std; int main(){ c ...
- java小项目之:扫雷,这游戏没有你想的那么简单!
扫雷 我之前分享的小项目和小游戏,电影购票.坦克大战.捕鱼达人.贪吃蛇等,虽然已经是耳熟能详人尽皆知的项目和游戏,但是保不齐真的有人没接触过. 今天分享的这个项目,我不相信没人接触过(仅限80后-00 ...
- 【转】Algorithms -离散概率值(discrete)和重置、洗牌(shuffle)算法及代码
离散概率值(discrete) 和 重置\洗牌(shuffle) 算法 及 代码 本文地址: http://blog.csdn.net/caroline_wendy/article/details/1 ...
- bzoj 2683 CDQ分治
题目描述 你有一个N*N的棋盘,每个格子内有一个整数,初始时的时候全部为0,现在需要维护两种操作: 命令 参数限制 内容 1 x y A 1<=x,y<=N,A是正整数 将格子x,y里的数 ...
- [apue] 一个查看当前终端标志位设置的小工具
话不多说,先看运行效果: >./term input flag 0x00000500 BRKINT not in ICRNL IGNBRK not in IGNCR not in IGNPAR ...
- CTF实验吧——证明自己吧
题目地址:http://www.shiyanbar.com/ctf/28 没有壳 ,vc++ 写的 拖进OD观察观察,发现代码很短哟,先来看这俩个call 怀疑他们其中有正确的flag和我们输入的东西 ...
- FastDF step by step
step one 肯定是安装一个FastDF服务了 step two FasDFS配置节点 step third 码代码