面霸的自我修养-synchronized

news2024/11/16 9:56:55

今天是《面霸的自我修养》的第3弹,内容是Java并发编程中至关重要的关键字synchronized,作为面试中的“必考题”,这部分是你必须要充分准备的内容,接下来我们就一起一探究竟吧。

数据来源:

  • 大部分来自于各机构(Java之父,Java继父,某灵,某泡,某客)以及各博主整理文档;
  • 小部分来自于我以及身边朋友的实际经理,题目上通过🔥来标识,并注明公司。

叠“BUFF”:

  • 八股文通常出现在面试的第一二轮,是“敲门砖”,但仅仅掌握八股文并不能帮助你拿下Offer;
  • 由于本人水平有限,文中难免出现错误,还请大家以批评指正为主,尽量不要喷~~
  • 本文及历史文章已经完成PDF文档的制作,提取关键字【面霸的自我修养】。

应用篇

🔥synchronized是什么?有什么作用(如何保证可见性,如何保证原子性)?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:美团,瑞幸

synchronized是Java提供的关键字,提供了原生同步机制,实现了互斥语义,保证了可见性,有序性和原子性。

有序性和原子性保证是互斥语义带来的,实现互斥的临界区,同一时间仅有一个线程可以执行,所以此时不存在有序性(编译器保证的as-if-serial语义)和原子性问题。

可见性保证是在退出synchronized时,使用了storeload内存屏障,ObjectMonitor::exit的源码如下:

void ObjectMonitor::exit(bool not_suspended, TRAPS) {
  for (;;) {
    if (Knob_ExitPolicy == 0) {
      OrderAccess::release_store(&_owner, (void*)NULL);
      OrderAccess::storeload();
    } else {
      OrderAccess::release_store(&_owner, (void*)NULL);
      OrderAccess::storeload();
    }
  }
}

storeload指令的执行顺序:

store1;
storeLoad;
load2;

保证load指令要“看”到store指令的最新值,因此store指令后,会将数据写回主内存,同时失效其它处理器中的数据。


🔥synchronized有哪些使用场景?编译后有什么区别?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:字节跳动,蚂蚁金服

synchronized可以用于修饰方法,或者代码块。

修饰方法时,编译后会添加ACC_SYNCHRONIZED标识,指明该方法是同步方法,JVM通过ACC_SYNCHRONIZED标识来判断是否执行同步调用。

public synchronized void method();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED

修饰代码块时,编译后在代码块开始和结束位置分别添加monitorentermonitorexit指令,执行monitorenter时获取锁,执行monitorexit指令时释放锁。

public void method();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    ......
    4: monitorenter
    ......
    43: athrow
    44: aload_1
    45: monitorexit
    46: goto          54
    49: astore_3
    50: aload_1
    51: monitorexit
    52: aload_3
    53: athrow
    54: return

monitorentermonitorexit总是成对执行的。实际上编译后的字节码中出现了两次monitorexit指令,第一次是在发生异常时,第二次是在正常结束时,看似出现两次,但也只会执行一次monitorexit指令。


🔥synchronized锁class对象,代表着什么?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:字节跳动

Java中每个对象都与一个监视器关联。synchronized锁定与对象关联的监视器(可以理解为锁定对象本身),锁定成功后才可以继续执行

synchronized锁定class对象时存在两种情况:

  • synchronized修饰静态方法,相当于锁定class对象;
  • synchronized锁定class对象。

它们效果是一样的,某一线程锁定class对象时,其它所有试图锁定class对象的线程都会被阻塞。比较特殊的一种情况:

public class Human {
  public void drink() {
    synchronized (Human.class) {
      // 业务逻辑
    }
  }
}

这种场景下,通过Human类不同实例对象并发调用Human#drink方法时,只有一个实例对象可以执行Human#drink方法,其余会被阻塞,从现象看起来是将Human类锁定,通常会成这种锁为类锁。


synchronized什么情况下是对象锁?什么情况下是类锁?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:无

锁定某个实例对象,或者使用synchronized修饰实例方法时,为对象锁。例如:

public class Human {
  public synchronized void eat() {
    // 业务逻辑
  }
  
  public void drink() {
    synchronized (this) {
      // 业务逻辑
    }
  }
}

两种场景实际上都是锁定实例对象

锁定class对象,或者使用synchronized修饰静态方法时,为类锁。例如:

public class Human {
  public static synchronized void eat() {
    // 业务逻辑
  }
  
  public void drink() {
    synchronized (Human.class) {
      // 业务逻辑
    }
  }
}

两种场景实际上都是锁定class对象


如果对象的多个方法添加了synchronized,那么对象有几把锁?

难易程度:🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:无

具体情况具体分析。如果只在静态方法上使用synchronized修饰,那么只有class对象上的一把锁;如果静态方法和实例方法都使用synchronized修饰,那么有两(多)把锁,class对象上的一把锁,当前对象(this对象)上一(多)把锁;修饰代码块时场景比较复杂,锁定几个对象,就有几把锁。


线程进入某个对象的synchronized实例方法后,其它线程是否可进入此对象的其它方法?

难易程度:🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:无

可以进入该对象未使用synchronized修饰的实例方法,对静态方法没有要求(前提是没有线程正在执行synchronized修饰的静态方法)。


对象的两个方法加synchronized,线程进入其中一个方法后执行Thread#sleep,其它线程可以进入到另一个方法吗?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:无

同一个实例对象调用时,会造成阻塞。不同实例对象调用时,不会造成阻塞。举个例子:

public class Human {
  public synchronized void eat() {
    System.out.println("[EAT]线程:" + Thread.currentThread().getName());
    TimeUnit.SECONDS.sleep(60);
  }
  
  public synchronized void drink() {
    System.out.println("[Drink]线程:" + Thread.currentThread().getName());
    TimeUnit.SECONDS.sleep(60);
  }
}

使用同一个实例对象调用:

public static void main(String[] args) throws InterruptedException {
  Human human = new Human();
  new Thread(human::eat, "t1").start();
  TimeUnit.SECONDS.sleep(1);
  
  new Thread(human::drink, "t2").start();
}

使用不同实例对象调用:

public static void main(String[] args) throws InterruptedException {
  Human human = new Human();
  new Thread(human::eat, "t1").start();
  TimeUnit.SECONDS.sleep(1);
  
  Human human2 = new Human();
  new Thread(human2::drink, "t2").start();
}

原理篇

🔥详细描述synchronized的实现原理。

难易程度:🔥🔥🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:蚂蚁,网易,腾讯,苏宁,美团,字节

synchronized是Java中的关键字,可以修饰方法或代码块,在修饰代码块的程序中,synchronized编译后会生成两个指令:monitorenter和monitorexit。

private static final Object locker = new Object();

private static  void lock() throws InterruptedException {
  synchronized (locker) {
	  locker.wait(2000);
  }
}

反编译后的字节码如下:

可以看到编译后字节码在第4,9和13行分别添加了monitorenter和monitorexit,表示着进入和退出synchronized保护的临界区,其中第9行和第13行的monitorexit分别为正常退出和异常时退出。

Tips:如果synchronized修饰的是方法,反编译后会添加ACC_SYNCHRONIZED标记,JVM通过该访问标记来实现synchronized的功能。

monitor(管程/监视器)

上述代码中,当程序执行到monitorenter时,需要获取到locker的monitor锁才可以执行临界区中的代码,而执行到monitorexit时,需要释放locker的monitor锁。

monitor通常被翻译为管程或监视器,是一种“更高层次”的同步机制。操作系统中,通常会直接支持Mutex和Semphore,而monitor则是编程语言在操作系统基础上所提供的同步机制,monitor提供了更简洁的操作方式,开发者无需关注acquire和release操作既可以完成同步操作。

Java中的ObjectMonitor

Java中,Monitor是通过ObjectMonitor实现的:

class ObjectMonitor {
  private:
    // 保存与ObjectMonitor关联Object的markOop
    volatile markOop   _header;
    // 与ObjectMonitor关联的Object
    void*     volatile _object;
  protected:
    // ObjectMonitor的拥有者
    void *  volatile _owner;
    // 递归计数
    volatile intptr_t  _recursions;
    // 等待线程队列,cxq移入/Object.notify唤醒的线程
    ObjectWaiter * volatile _EntryList;
  private:
    // 竞争队列
    ObjectWaiter * volatile _cxq;
    // ObjectMonitor的维护线程
    Thread * volatile _Responsible;
  protected:
    // 线程挂起队列(调用Object.wait)
    ObjectWaiter * volatile _WaitSet;
}

其工作原理如下:

想要获取monitor的线程会首先进入到_WaitSet中,当获取到monitor后,将ObjectMon的_recursions加1(重入特性的实现),并将_owner设置为当前的线程,允许线程进入临界区执行代码。


🔥详细描述下synchronized的锁升级(膨胀)过程。

难易程度:🔥🔥🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:网易,字节跳动,盒马

Java 6之前,synchronized通过调用操作系统的Mutex Lock来实现互斥功能,此时应用程序需要从用户态切换到内核态,这个过程带来了一定的性能损失。

除此之外,每次进入synchronized保护的临界区时,无论是否发生竞争,都会使用Mutex Lock来保证同一时间只有一个线程进入临界区,即便此时只有一个线程访问这段代码。

Java 6之后,为了优化synchronized的性能问题引入了锁升级机制,此时的synchronized实际由3把锁组成:偏向锁轻量级锁和重量级锁。synchronized的升级机制使用了Mark Word,以下是64位大端模式下Mark Word在不同锁时的情况:

偏向锁

顾名思义,偏向锁会倾向于第一个访问的线程。如果在程序的运行中,只有一个线程访问由synchronized修饰的代码块不存在任何竞争,此时只需要修改对Mark Word的锁标记位即可,而无需通过Mutex实现加锁。

偏向锁的获取流程:

  1. 检查Mark Word的锁标记位是否可偏向;
  2. 如果可偏向,则尝试将线程ID执行当前线程,失败进入步骤3,成功进入步骤5;
  3. 通过CAS尝试替换线程ID,替换成功进入步骤5,否则进入步骤4;
  4. CAS替换线程ID失败,则表示当前锁有竞争,此时会升级为轻量级锁。
  5. 执行synchronized修饰的临界区代码。
轻量级锁

当有其它线程进入synchronized后,偏向锁会升级为轻量级锁,注意此时并不存在竞争,而是线程的交替执行。

轻量级锁的获取流程:

  1. 在当前线程的栈帧中建立锁记录(Lock Record),存储Monitor对象的Mark Word的拷贝;
  2. 通过CAS操作,将Monitor对象的Mark Word指向Lock Record,并更新锁标志位;
  3. 步骤2执行成功后,线程就可以执行synchronized修饰的临界区代码了,否则进入步骤4;
  4. 如果Mark Word记录了指向当前线程Lock Record的指针,则可以直接执行临界区代码,否则说明存在竞争,轻量级锁升级为重量级锁。
重量级锁

当同一时间有多个线程竞争时,轻量级锁升级为重量级锁,此时是通过调用操作系统的Mutex实现的同步机制。


为什么说synchronized是悲观锁?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:无

悲观锁认为并发访问共享总是会发生修改,因此在进入临界区前一定会执行加锁操作。对于synchronized来说,无论是偏向锁,轻量级锁还是重量级锁,使用synchronized总是会发生加锁,因此是悲观锁。


为什么说synchronized是非公平锁?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:无

非公平性体现在发生阻塞后的唤醒并不是按照先来后到的顺序进行的。在synchronized中,默认策略是将cxq队列中的数据移入到EntryList后再进行唤醒,并没有按照先后顺序执行。实际上我们也不知道cxqEntryList中的线程到底谁先进入等待的。


为什么说synchronized是可重入锁?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:无

可重入指的是允许同一个线程反复多次加锁。使用上,synchronized允许同一个线程多次进入。底层实现上,synchronized内部维护了计数器_recursions,发生重入时,计数器+1,退出时计数器-1。通过_recursions的命名,我们也能知道Java中的可重入锁就是POSIX中的递归锁。


锁消除是什么?锁粗化是什么?

难易程度:🔥🔥🔥

重要程度:🔥🔥🔥🔥🔥

公司:无

锁消除(Lock Elimination)即JVM删除不必要的加锁操作。根据逃逸分析技术,如果加锁部分代码不会逃逸出当前线程,即只有一个线程会访问到加锁部分代码,JVM会认为当前代码是线程安全的,而加锁操作是不必要的进而删除加锁操作。

public void append(String str1, String str2) {
  StringBuffer sb = new StringBuffer();
  sb.append(str1).append(str2);
}

虽然StringBuffer#append方法使用synchronized修饰,但因为sb对象是局部变量,不会从该方法中逃逸,因此该方法是线程安全的,可以进行锁消除。锁粗化即将多次加锁操作合并为一次,将多个连续加锁操作合并成一次范围更大的加锁操作。

StringBuffer stringBuffer = new StringBuffer();

public void append() {
  sb.append("w");
  sb.append("y");
  sb.append("z");
}

每次调用StringBuffer#append方法都会进行加锁和解锁操作,如果JVM检测到连续的对同一个对象的加锁和解锁操作,就会合并成一次范围更大的加锁和解锁操作,即第一次执行StringBuffer#append方法时加锁,最后一次执行StringBuffer#append时解锁。

参考

  • 管程(维基百科)
  • 关于线程你必须知道的8个问题(下)
  • 一文看懂并发编程中的锁
  • synchronized都问啥?
  • 从源码揭秘偏向锁的升级
  • 什么是synchronized的重量级锁?
  • 关于synchronized的一切,我都写在这里了

如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠王有志,我们下次再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/947609.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringWeb(SpringMVC)

目录 SpringWeb介绍 搭建 SpringWeb SpringWeb介绍 Spring Web是一个基于 Servlet API 构建的原始 web 框架,用于构建基于MVC模式的Web应用程序。在 web 层框架历经 Strust1,WebWork,Strust2 等诸多产品的历代更选 之后,目前业界普…

研发规范第九讲:通用类命名规范(重点)

研发规范第九讲:通用类命名规范(重点) 无规范不成方圆。我自己非常注重搭建项目结构的起步过程,应用命名规范、模块的划分、目录(包)的命名,我觉得非常重要,如果做的足够好&#xff…

聊聊大厂都怎么防止重复下单?

一、问题背景 最简单的:DB 事务。如创建订单时,同时往订单表、订单商品表插数据,这些 Insert 须在同一事务执行。 Order 服务调用 Pay 服务,刚好网络超时,然后 Order 服务开始重试机制,于是 Pay 服务对同一…

java八股文面试[多线程]——自旋锁

优点: 1. 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗 ,这些操作会导致线程发生两次上下文切换&…

【Python自学笔记】Python好用的模块收集(持续更新...)

文章目录 日志模块钉钉机器人命令助手持续更新中,如果您有其他实用好用的模块欢迎留言...日志模块 写代码离不开日志,自定义一个理想的日志对于小白来说可能是一件很反锁的事情,就像我刚学习Python的时候自己写的一个自定义日志,为了解决这个痛点,今天就和大家分享一个可以…

Linux开发板下检查及配置串口(波特率/数据发送接收等)的操作

在linux开发板上如何设置和编辑串口波特率、开启指定的uart口? 下面演示常用的命令操作 1)编辑开启指定串口的配置文件 ls -l /boot/uEnv/ sudo vim /boot/uEnv/xxx.txt 2)检查串口是否开启成功 ls /dev/tty* 3)查看串口波特率…

每天 26,315 美元罚款?交通安全局要求特斯拉提供 Autopilot数据

根据美国国家公路交通安全管理局(NHTSA)最近的特别命令,特斯拉公司被要求提供关于其自动驾驶功能Autopilot的相关信息。这一命令是继NHTSA于2021年8月启动初步评估后,在2022年6月升级为正式调查的一部分,NHTSA近期对特…

电子电路学习笔记之NCP304LSQ37T1G ——超低电流电压检测器

超低电流电压检测器是一种专门用于检测极小电流值的设备。它们常用于电子元件或电路中,用于监测电流的存在和程度。这些检测器通常具有高灵敏度和高精度,能够测量微安级别或更小的电流。 超低电流电压检测器的应用领域广泛,例如电池管理系统…

宏昌转债上市价格预测

宏昌转债 基本信息 转债名称:宏昌转债,评级:A,发行规模:3.8亿元。 正股名称:宏昌科技,今日收盘价:30.5元,转股价格:29.62元。 当前转股价值 转债面值 / 转股…

Python绘图系统11:绘制极坐标图像

文章目录 旧代码整改投影下拉选框绘图逻辑源代码 Python绘图系统: 📈从0开始实现一个三维绘图系统自定义控件:坐标设置控件📉坐标列表控件📉支持多组数据的绘图系统📉极坐标绘图图表类型和风格&#xff1a…

C#,《小白学程序》第九课:堆栈(Stack),先进后出

1 文本格式 /// <summary> /// 《小白学程序》第九课&#xff1a;堆栈&#xff08;Stack&#xff09; /// 堆栈与队列是相似的数据形态&#xff1b;特点是&#xff1a;先进后出&#xff1b; /// 比如&#xff1a;狭窄的电梯&#xff0c;先进去的人只能最后出来&#xff1…

【教程】DGL中的子图分区函数partition_graph讲解

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 目录 函数形式 函数作用 函数内容 函数入参 函数返参 使用示例 实际上官方的函数解释中就已经非常详细了。 函数形式 def partition_graph(g, graph_name, num_parts, out_path, num_hops1, part…

聚观早报|OpenAI宣布推出企业版ChatGPT;苹果公司开设8家新店

【聚观365】8月30日消息 OpenAI宣布推出企业版ChatGPT 比亚迪上半年净利润109.5亿元 歌尔股份上半年净利润4.22亿元 一起教育科技Q2营收6925万元 苹果公司今年开设8家新店 OpenAI宣布推出企业版ChatGPT 据外媒报道&#xff0c;当地时间周一&#xff0c;美国人工智能研究…

用XSIBackup为VMware ESXi打造完美备份方案

文章目录 VMware ESXi 备份方案引言XSIBackup安装步骤1. XSIBackup软件安装2. SSH连接3. 定位到xsibackup目录4. 修改文件权限5. 安装cron查看crontab列表6. 配置备份任务结论VMware ESXi 备份方案 引言 数据就像是我们的生命线,一旦丢失,可能会带来无法挽回的损失。对于那…

2024王道408数据结构P144 T16

2024王道408数据结构P144 T16 思考过程 首先看题目&#xff0c;要求我们把二叉树的叶子结点求出来并且用链表的方式存储&#xff0c;链接时用叶结点的右指针来存放单链表指针。我们很清楚可以看出来能用中序遍历递归的方式实现&#xff0c;因为第一个叶子结点在整棵树的最左下…

2024年java面试--集合篇

文章目录 前言ListSetMapCollectionListSetMapJDK1.7 HashMap&#xff1a;JDK1.8 HashMap&#xff1a; 一、ArrayList和LinkedList的区别二、HashSet的实现原理&#xff1f;三、List接口和Set接口的区别四、hashmap底层实现五、HashTable与HashMap的区别六、线程不安全体现七、…

基于Java+SpringBoot+Vue前后端分离医药管理系统设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

无涯教程-Android - List fragments函数

框架的ListFragment的静态库支持版本&#xff0c;用于编写在Android 3.0之前的平台上运行的应用程序&#xff0c;在Android 3.0或更高版本上运行时,仍使用此实现。 List fragment 的基本实现是用于创建fragment中的项目列表 List in Fragments 示例 本示例将向您说明如何基于…

基于饥饿游戏算法优化的BP神经网络(预测应用) - 附代码

基于饥饿游戏算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于饥饿游戏算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.饥饿游戏优化BP神经网络2.1 BP神经网络参数设置2.2 饥饿游戏算法应用 4.测试结果&#xff1a;5…

基于Java+SpringBoot+Vue前后端分离文理医院预约挂号系统设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…