前言

说起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保证线程安全的基础。

  1. 对于同步方法,锁是当前实例对象,即this,对该类其他实例对象无影响。

  2. 对于静态同步方法,锁是当前对象的 Class 对象, 影响其他该类的实例化对象。

  3. 对于同步方法块,锁是 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

http://cmsblogs.com/?p=2071

https://www.php.cn/java-article-410323.html

本文由博客一文多发平台 OpenWrite 发布!

文章首发:https://zhuanlan.zhihu.com/lovebell

个人公众号:技术Go

您的点赞与支持是作者持续更新的最大动力!

【搞定面试官】- Synchronized如何实现同步?锁优化?(1)的更多相关文章

  1. 搞定面试官 - MySQL 中你知道如何计算一个索引的长度嘛?

    大家好,我是程序员啊粥. 今天给大家分享一个我遇到过的比较少见的面试题,那就是 MySQL 中如何计算一个索引的长度. 说实话,我第一次遇到这个问题的时候想当然的以为索引长度就是我们建表时定义的字段长 ...

  2. 搞定面试官 - 你可以介绍一下在 MySQL 中,哪些情况下 索引会失效嘛?

    大家好,我是程序员啊粥,前边给大家分享了 *MySQL InnoDB 索引模型 在 MySQL InnoDB 中,为什么 delete 删除数据之后表数据文件大小没有变 如何计算一个索引的长度 如何查 ...

  3. 金三银四,2018最新iOS面试题,由它可以搞定面试官?

    序言 这些资料,你一定会用到!我相信很多人都在说,iOS行业不好了,iOS现在行情越来越难了,失业的人比找工作的人还要多.失业即相当于转行,跳槽即相当于降低自己的身价.那么做iOS开发的你,你是否在时 ...

  4. 【搞定面试官】try中有return,finally还会执行吗?

    本篇文章我们主要探讨 一下如果try {}语句中有return,这种情况下finally语句还会执行吗?其实JVM规范是对这种情况有特殊规定的,那我就先上代码吧! public class Final ...

  5. 搞定面试官:咱们从头到尾再说一次 Java 垃圾回收

    接着前几天的两篇文章,继续解析JVM面试问题,送给年后想要跳槽的小伙伴 万万没想到,面试中,连 ClassLoader类加载器 也能问出这么多问题..... 万万没想到,JVM内存区域的面试题也可以问 ...

  6. RabbitMQ:从入门到搞定面试官

    安装 使用docker安装,注意要安装tag后缀为management的镜像(包含web管理插件),我这里使用的是rabbitmq:3.8-management 1. 拉取镜像 shell docke ...

  7. 【搞定面试官】谈谈你对JDK中Executor的理解?

    ## 前言 随着当今处理器计算能力愈发强大,可用的核心数量越来越多,各个应用对其实现更高吞吐量的需求的不断增长,多线程 API 变得非常流行.在此背景下,Java自JDK1.5 提供了自己的多线程框架 ...

  8. 【搞定面试官】你还在用Executors来创建线程池?会有什么问题呢?

    前言 上文我们介绍了JDK中的线程池框架Executor.我们知道,只要需要创建线程的情况下,即使是在单线程模式下,我们也要尽量使用Executor.即: ExecutorService fixedT ...

  9. 搞定面试官 - 可以介绍一下在 MySQL 中你平时是怎么使用 COUNT() 的嘛?

    大家好,我是程序员啊粥. 相信在大家的工作中,有很多的功能都需要用到 count(*) 来统计表中的数据行数.同时,对于一些大数据的表,用 count 都是瑟瑟发抖,往往会结合缓存等进行处理. 那么, ...

随机推荐

  1. 数据库中间件分片算法之enum

    前言 最近挺焦虑的,不知道未来该做什么,方向又是什么.只能用别慌,月亮也正在大海的某处迷茫.来安慰下自己.不过学习的初心咱们还是不要忘记.今天我们学习的是enum分片算法. 1.hash分区算法 2. ...

  2. 小小知识点(十四)——Adobe photoshop cc 2018中简单抠图的一些基本操作

    一 如何抠图 1. 右键弹出选择工具,随后鼠标左键选择快速选择工具 2.通过点击鼠标,选择想要的区域: Alt+鼠标右键  左右拖动鼠标可调整画笔大小 Alt+鼠标滑轮,可放大或缩小画布大小 ctrl ...

  3. angular.foreach 循环方法

    angular循环给一个 angular监听的变量复值时.最好还是用angular自带的循环方法.“angular.foreach” 尽量避免代码的冲突,最好不要jq angular 混用 var o ...

  4. Springboot Jackson配置根本方案, 日期格式化, 时区设置生效

    当项目集成配置的功能越来越多, 说不准哪个配置就影响到了什么. 比如你启用了EnableMvC, 默认配置文件配置的一些文件就失效了. 虽然约定大于配置,让springboot可以极简化构建, 但不熟 ...

  5. 记录一次mybatis genertor使用以及mapper扫描遇见的问题

    本记录适用初次接触mybatis,大神忽略... 整体上分两个部分: 1.使用mybatis genertor自动生成代码 2.mapper的扫描 1.使用mybatis genertor自动生成代码 ...

  6. EntityFramework Core表名原理解析,让我来,揭开你神秘的面纱

    前言 上一节我们针对最开始抛出的异常只是进行了浅尝辄止的解析,是不是有点意犹未尽的感觉,是的,我也有这种感觉,看到这里相信您和我会有一些疑惑,要是我们接下来通过注解.Fluent APi.DbSet分 ...

  7. 更好用的 Python 任务自动化工具:nox 官方教程

    英文| nox tutorial 出处| nox 官方文档 译者| 豌豆花下猫@Python猫 Github地址:https://github.com/chinesehuazhou/nox_doc_c ...

  8. 深入理解 Java 并发锁

    本文以及示例源码已归档在 javacore 一.并发锁简介 确保线程安全最常见的做法是利用锁机制(Lock.sychronized)来对共享数据做互斥同步,这样在同一个时刻,只有一个线程可以执行某个方 ...

  9. Python使用requests爬取一个网页并保存

    #导入 requests模块import requests #设置请求头,让网站监测是浏览器 headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 6. ...

  10. bzoj1597: [Usaco2008 Mar]土地购买 dp斜率优化

    东风吹战鼓擂第一题土地购买送温暖 ★★★   输入文件:acquire.in   输出文件:acquire.out   简单对比时间限制:1 s   内存限制:128 MB 农夫John准备扩大他的农 ...