王有志,一个分享硬核Java技术的互金摸鱼侠

加入Java人的提桶跑路群:共同富裕的Java人

今天是《面霸的自我修养》的第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时解锁。

参考


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

面霸的自我修养:synchronized专题的更多相关文章

  1. GIS制图人员的自我修养(1)--制图误区

    GIS制图人员的自我修养 by 李远祥 最近一直坚持写GIS制图的技术专题,并不是为了要介绍有什么好的技术和方法去制图,而是要告诉所有从事这一方向的人员一个铁铮铮的实现--要做好GIS制图,必须加强自 ...

  2. IT技术管理者的自我修养

    1. 前言 本来写<IT技术管理者的自我修养>与<IT技术人员的自我修养>是一开始就有的想法.但发表<IT技术人员的自我修养>后,收到了不少良好的反馈,博客园的编辑 ...

  3. 《web全栈工程师的自我修养》读书笔记

    有幸读了yuguo<web全栈工程师的自我修养>,颇有收获,故在此对读到的内容加以整理,方便指导,同时再回顾一遍书中的内容. 概览 整本书叙述的是作者的成长经历,通过经验的分享,给新人或者 ...

  4. 程序员的自我修养(2)——计算机网络(转) good

    相关文章:程序员的自我修养——操作系统篇 几乎所有的计算机程序,都会牵涉到网络通信.因此,了解计算机基础网络知识,对每一个程序员来说都是异常重要的. 本文在介绍一些基础网络知识的同时,给出了一些高质量 ...

  5. GIS制图人员的自我修养(2)--制图意识

    GIS制图人员的自我修养(2)--制图意识 by 李远祥 上次提及到GIS制图人员的一些制图误区,主要是为GIS制图人员剖析在制图工作中的一些问题.但如何提高制图的自我修养,却是一个非常漫长的过程,这 ...

  6. web性能优化 来自《web全栈工程师的自我修养》

    最近在看<web全栈工程师的自我修养>一书,作者是来自腾讯的前端工程师.作者在做招聘前端的时候问应聘者web新能优化有什么了解和经验,应聘者思索后回答“在发布项目之前压缩css和 Java ...

  7. gcc ld 链接器相关知识,调试指令(程序员的自我修养----链接、装载与库)

    最近解决一个动态链接上的问题,因为以前从来没有接触过这方面的知识,所以恶补了一下,首先要了解gcc编译指令(makefile),ld链接器的选项(还有连接脚本section指定内存位置),熟悉查看连接 ...

  8. Python学习笔记(四十九)爬虫的自我修养(一)

    论一只爬虫的自我修养 URL的一般格式(带括号[]的为可选项): protocol://hostname[:port]/path/[;parameters][?query]#fragment URL由 ...

  9. Hacker的社交礼仪与自我修养【转】

    Hacker School是位于纽约的一所特殊的编程“学校”,他们的目标是帮助参与者变成“更好的程序员”,之所以说他们特殊是因为这所“学校”没有老师,没有考试,也不会颁发证书,他们信奉三人行必有我师, ...

  10. 第八周读书笔记(人月神话X月亮与六便士)——到底什么才是一个程序员的自我修养?

    写了这么久的读书笔记,涉及到问题大多是一些如何把软件工程做好,如何把自己的职业生涯做好.但总感觉逻辑链上缺了一环,亦即:我们为什么要把软件工程做好,我们成为一名优秀的职业生涯的意义到底在于什么?我觉得 ...

随机推荐

  1. 2021-10-04:解码方法 II。‘A‘ -> 1,‘B‘ -> 2,...‘Z‘ -> 26。*是1-9,不包含0。给你一个字符串 s ,由数字和 ‘*‘ 字符组成,返回 解码 该字符串的方法

    2021-10-04:解码方法 II.'A' -> 1,'B' -> 2,-'Z' -> 26.是1-9,不包含0.给你一个字符串 s ,由数字和 '' 字符组成,返回 解码 该字符 ...

  2. docker安装kibana,报错Kibana server is not ready yet,未解决

    1.命令 docker run -d -e ELASTICSEARCH_URL=http://192.168.101.158:9200 -p 5601:5601 --name kibana kiban ...

  3. phpstudy-sqlilabs-less-14

    题目:POST - Double Injection - Single quotes- String - with twist 和上关一模一样 uname=1"or 1=1 #&pa ...

  4. 2013年蓝桥杯C/C++大学B组省赛真题(马虎的算式)

    题目描述: 小明是个急性子,上小学的时候经常把老师写在黑板上的题目抄错了. 有一次,老师出的题目是:36 x 495 = ? 他却给抄成了:396 x 45 = ?  但结果却很戏剧性,他的答案竟然是 ...

  5. sipp重放rtp数据测试FreeSWITCH

    环境:CentOS 7.6_x64 FreeSWITCH版本 :1.10.9 sipp版本:3.6.1 一.背景描述 sipp是一款VoIP测试工具,日常开发过程中会使用到该软件,但其自身携带的pca ...

  6. Java的jps命令使用详解

    jps命令简介 jps(Java Virtual Machine Process Status Tool)是JDK提供的一个可以列出正在运行的Java虚拟机的进程信息的命令行工具,它可以显示Java虚 ...

  7. R 语言常用操作与函数汇总

    总结了一下 R 语言中常用的一些操作与函数使用,抛砖引玉,分享一下给大家,如有错误的地方欢迎留言指正. 怎样显示 R 软件中某个包中包含的全部数据集? > library(MASS)> d ...

  8. Python正则表达式完全指南

    本篇文章将深入探讨python的一项强大工具:正则表达式.正则表达式是一个强大的文本处理工具,可以用来匹配,搜索,替换和解析文本.我们将逐步展示如何在Python中使用正则表达式,包括其基本语法,常见 ...

  9. 【城南 · LlamaIndex 教程】一文看懂LlamaIndex用法,为LLMs学习私有知识

    我是卷了又没卷,薛定谔的卷的AI算法工程师「陈城南」(全网平台同名)~ 担任某大厂的算法工程师,带来最新的前沿AI知识,分享 AI 有趣工具和实用玩法,包括 ChatGPT.AI绘图等,欢迎大家交流~ ...

  10. Leecode SQL

    618 学生地理信息报告 一所学校有来自亚洲.欧洲和美洲的学生.写一个查询语句实现对大洲(continent) 列的透视表操作,使得每个学生按照姓名的字母顺序依次排列在对应的大洲下面.输出的标题应依次 ...