2024-03-23  阅读(2)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/1775246437

回答

synchronized 是 Java 中一个重量级的关键字,它用于实现线程同步,确保多线程环境下对共享资源的安全访问。它可以修饰方法或代码块,保证一次只有一个线程可以执行同步方法或代码块内的代码。

synchronized有两种形式上锁,一个是对方法上锁,一个是对代码块上锁。其实他们底层实现原理都是一样的。在进入同步代码之前先获取锁,锁计数 + 1,执行完同步代码后释放锁,锁计数 -1,如果获取失败就阻塞式等待锁的释放。他们的不同之处在于他们在同步块的识别方式有所不同。

当一个方法被 synchronized 修饰时,它的方法标志中会包含 ACC_SYNCHRONIZED 标志。当某个线程要访问方法时,会首先检查是否有 ACC_SYNCHRONIZED 设置,如果有,则需要先获取监视器锁,获取成功后才能执行方法,方法执行完成后再释放监视器锁。如果在该线程执行同步方法期间,有其他线程来请求执行方法,会因为无法获取监视器锁而阻塞。

而同步代码块则是使用 monitorenter 和 monitorexit 指令来实现的。我们可以理解执行 monitorenter 为加锁,执行 monitorexit 为释放锁。每个对象都维护着一个锁的计数器,为被锁定的对象该计数器为 0。当一个线程在执行 monitorenter 之前需要尝试后去锁,如果这个对象没有被锁定,或者当前线程已经拥有了该对象的锁,那么这把锁的计数器 + 1,当执行monitorexit指令时,锁的计数器也会减1。

扩展

基本使用

Synchronized 是 Java 中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:

  1. 原子性:确保线程互斥的访问同步代码
  2. 可见性:保证共享变量的修改能够及时可见。
  3. 有序性:有效解决重排序问题。

从语法上讲,Synchronized 可以把任何一个非null对象作为 “锁”,在HotSpot JVM实现中,这个锁有个专门的名字:对象监视器(Object Monitor)。

从语法上讲,synchronized总共有三种用法,三种用法锁定的对象都不同:

  • synchronized 用于实例方法时,监视器锁(monitor)便是对象实例(this)。
public synchronized void method() {
    // 方法体
}

锁定的是调用该方法的对象实例。

  • Synchronized 用于静态方法时,监视器锁(monitor)便是对象的Class实例。
public static synchronized void staticMethod() {
    // 方法体
}

锁定的是这个类的所有对象。

  • Synchronized 用于同步代码块时,监视器锁(monitor)便是括号括起来的对象实例。
public void method() {
    synchronized (object) {
        // 代码块
    }
}

同步实现原理

同步需要依赖锁,那锁的同步又依赖谁?synchronized 给出的答案是在软件层面依赖JVM。我们来看同步方法和同步代码块是如何实现的。

同步方法

代码如下:

public class SynchronizedTest {
    public synchronized void test() {
        System.out.println("死磕 Java 面试...");
    }
    
    // 为了对比
    public  void test1() {
        System.out.println("死磕 Java 面试...");
    }
}

使用 javac SynchronizedTest.java,编译 Java 文件为 .class 文件,然后使用 javap -verbose SynchronizedTest,结果如下:

对于普通方法,其实就是常量池中多了 ACC_SYNCHRONIZED 标识符,JVM就是根据该标示符来实现方法的同步的:

当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor ,获取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

同步代码块

代码如下:

public class SynchronizedTest {
    public void test() {
        synchronized (this) {
            System.out.println("死磕 Java 面试...");
        }
    }
}

反编译 .class 文件,内容如下:

如上面所提到的执行 monitorenter 为加锁,执行 monitorexit 为释放锁。

每个对象都是一个监视器锁。当monitor被占用时就会处于锁定状态。

执行 monitorenter 过程如下:

  1. 如果 monitor 的计数为 0,说明锁未被持有,JVM 将锁分配给执行 monitorenter 的线程,并将 monitor 的计数设置为 1。
  2. 如果线程已经占有了该 monitor,当前线程重入,monitor 的计数 + 1。
  3. 如果 monitor 被其他线程持有,那么当前线程将被阻塞,直到锁被释放。

执行 monitorexit 的过程:

  • 执行 monitorexit 的线程必须是对应 monitor 的所有者,即执行 monitorexitmonitorenter 要是同一个线程。
  • 执行 monitorexit 时,monitor 的计数器 - 1,如果计数器大于 0,表示当前线程还持有 monitor(可重入),锁不会被释放,如果计数器等于 0,表示当前线程不再持有 monitor ,锁被释放。
  • monitorexit 指令出现两次,原因是为了兼顾执行同步代码时出现异常而导致锁无法释放的问题。所以第1次为同步正常退出释放锁,第2次为发生异步退出释放锁。

监视器(monitor)

synchronized在 JVM 中的实现都是基于进入和退出monitor对象来实现方法同步和代码块同步,所以我们有必要来了解下 monitor。

那什么是 monitor 呢?我们可以把它理解为一种同步机制,它通常被描述为一个对象。

我们知道,在 Java 中一切皆对象,同理,在 Java 中所有的 Java 对象是天生的 monitor,每一个 Java 对象都有成为 monitor 的可能。这是因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁**,它叫内置锁**。

monitor 由 ObjectMonitor 实现,其主要数据结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;   // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

几个重要属性如下:

  • _header:指向对象头,在 Java 中,每个对象都有一个对象头,其中包含了与锁和垃圾收集相关的信息。
  • _count:用于记录重入锁的数量。在 Java 中,同一个线程可以多次获得同一个锁(即重入锁),这个字段就是用来记录该线程获取锁的次数。
  • _recursions:用于记录同一个线程重复获取这个锁的次数。
  • _waiters:记录正在等待获取这个对象锁的线程数量。
  • _owner:当前拥有这个 monitor 的线程
  • _WaitSet:处于 wait 状态的线程,会被加入到 _WaitSet 中,可通过 notify()notifyAll() 唤醒。
  • _EntryList:处于等待锁 block 状态的线程,会被加入到 _EntryList 中。

ObjectMonitor 中有两个队列,_WaitSet_EntryList,用来保存 ObjectWaiter 对象列表( 每个等待锁的线程都会被封装成 ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程访问一段同步代码时:

  1. 首先会进入 _EntryList 集合,当线程获取到对象的 monitor 后,进入 _owner 区域并把 monitor 中的_owner变量设置为当前线程,同时monitor中的计数器_count加1;
  2. 若持有 monitor 的线程调用 wait(),将释放当前持有的monitor,_owner变量恢复为null,_count自减1,同时该线程进入 _WaitSet集合中等待被唤醒;
  3. 若持有 monitor 的线程执行完毕,也将释放当前持有的 monitor,并复位变量的值,以便其他线程进入获取monitor;


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文