写点什么

是时候来唠一唠 synchronized 关键字了,Java 多线程的必问考点!

作者:EquatorCoco
  • 2024-03-25
    福建
  • 本文字数:2146 字

    阅读完需:约 7 分钟

写在开头


在之前的博文中,我们介绍了 volatile 关键字,Java 中的锁以及锁的分类,今天我们花 5 分钟时间,一起学习一下另一个关键字:synchronized


synchronized 是什么?


首先synchronized是 Java 中的一个关键字,所谓关键字,就是 Java 中根据底层封装所赋予的一种具有特殊语义的单词,而 synchronized 译为同步之意,可保证在同一时刻,被它修饰的方法或代码块只能有一个线程执行,它的使用解决了并发多线程中的三大问题:原子性、可见性、顺序性

很多小伙伴在过往的书籍中可能会看到说 synchronized 是一种重量级锁,性能差,不建议在代码中使用,其实这是早期的 synchronized 特点,自 JDK1.6 之后,synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized 锁的效率提升了很多。因此, synchronized 还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 synchronized 。


synchronized 的使用


synchronized 在 Java 中主要的 3 种使用方式:

  1. 修饰实例方法: 为当前对象实例加锁,进入同步方法需要先获取对象锁;

  2. 修饰静态方法: 为当前类加锁,锁定的是 Class 对象,进入同步方法需要先获取类锁;

  3. 修饰代码块: 为指定对象加锁,进入同步方法需要先获取指定对象的锁。


样例:

//修饰实例方法,为当前实例加锁synchronized void method() {    //业务代码}//修饰静态方法,锁为当前Class对象synchronized static void method() {    //业务代码}//修饰代码块,锁为括号里面的对象synchronized(this) {    //业务代码}
复制代码


写到这里,突然想到了 3 个面试可能会考的知识点,列举一下!


问题 1:synchronized 修饰代码块可以给类加锁吗?

当然可以!我们前面说了修饰代码块时,是给代码中的对象加锁,这里面的对象既可以是实例也可以是类。


问题 2:静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?

不互斥!如果线程 A 调用一个实例对象的非静态 synchronized 方法,线程 B 同时去调用这个实例对象所属类的静态 synchronized 方法并不会发生互斥,因为线程 A 此时拿到的是实例对象锁,而线程 B 拿到的是当前类的锁。


问题 3:构造方法可以用 synchronized 修饰么?

不可以!构造方法本身就是线程安全的,在 Java 开发规范里也明确告诉我们

构造方法不能是抽象的(abstract)、静态的(static)、最终的(final)、同步的(synchronized)。


synchronized 的底层原理


在 synchronized 的底层(JVM 层面),针对方法与代码块的实现逻辑是不同的,因此我们在分析底层原理是也要分别来看。

1、当 synchronized 修饰方法时

public class Test {    public synchronized void method() {        System.out.println("synchronized 方法");    }}
复制代码


我们通过对编译后的 class 文件进行反编译后,分析其底层实现。

知识点扩展:我们通过 javap 命令进行反编译,javap 是 Java class 文件分解器,可以反编译,也可以查看 java 编译器生成的字节码等。javap 参数如下:



使用方式,既可以在电脑的命令行提示符中使用,也可以通过 idea 的 terminal 终端使用,我这里采用 idea 中进行反汇编操作。参考命令:javap -c -v Test.class


【反汇编结果】



由上图可看出同步方法通过加 ACC_SYNCHRONIZED 标识实现线程的执行权的控制,如果修饰的是实例方法,JVM 会获取对象锁,如果修饰的是静态方法,JVM 会获取当前类锁。


2、当 synchronized 修饰代码块时


public class Test {    public void method() {        synchronized (this) {            System.out.println("synchronized");        }    }}
复制代码


【反汇编结果】



与同步方法不同,同步代码块中使用了 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置,并且 monitorexit 标识有 2 个,以保证在正常执行和异常情况下均可释放锁。

在命令执行到 monitorenter 时,线程会去尝试获取对象得锁,这里也可称之为对象所对应的 monitor 所有权。写到这里,我们又要做一个知识点扩展啦。


知识点扩展:

在 JVM 中 monitor 的底层基于 C++实现,被称之为对象监视器,每个对象都会内置一个 ObjectMonitor 与之关联,关联的起始地址存于对象头的 MarkWord 中。



ObjectMonitor 几个关键属性:

  • _owner:指向持有 ObjectMonitor 对象的线程

  • _WaitSet:存放处于 wait 状态的线程队列

  • EntryList:存放处于等待锁 block 状态的线程队列

  • recursions:锁的重入次数

  • _count:用来记录该线程获取锁的次数


当多个线程同时访问同步代码时,会被放入 EntryList 中,根据线程优先级尝试获取对象锁,如果锁的计数器为 0 则表示锁可以被获取,获取到锁的线程进入 owner 区域,count 加 1,这里其实还有之前说的 object 中的 wait/notify/notifyall 的组合,也依赖 monitor,所以他们才必须用在同步方法或代码块中。对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。


总结


关于 synchronized 的介绍其实远没有结束,还有很多细节可以值得学习,我们会在后面的文章中逐渐补充,避免文章过长,读者失去阅读的耐心!


文章转载自:JavaBuild

原文链接:https://www.cnblogs.com/JavaBuild/p/18090903

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
是时候来唠一唠synchronized关键字了,Java多线程的必问考点!_Java_EquatorCoco_InfoQ写作社区