《面试1v1》
  5YI10LEk8YTp 2023年11月13日 13 0

我是 javapub,一名 Markdown 程序员从👨💻,八股文种子选手。


面试官:你好,我看到你的简历上写着你熟悉 Java 中的 "synchronized" 关键字。你能给我讲讲它的作用吗?

候选人: 当然,"synchronized" 是 Java 中的一个关键字,用于实现同步机制。它可以用来修饰方法或代码块,以确保在同一时间只有一个线程可以访问被修饰的代码。

面试官:很好。那么,你能举个例子来说明 "synchronized" 关键字的使用方法吗?

候选人: 当然。你可以使用 "synchronized" 关键字来修饰方法或代码块。例如,你可以这样使用:

public synchronized void doSomething() {
    // ...
}

在上面的代码中,"synchronized" 关键字修饰了 "doSomething()" 方法。这意味着在同一时间只有一个线程可以访问该方法。

面试官:很好。那么,如果我想修饰一个代码块,应该怎么做呢?

候选人: 你可以这样使用 "synchronized" 关键字来修饰一个代码块:

public void doSomething() {
    synchronized (this) {
        // ...
    }
}

在上面的代码中,"synchronized" 关键字修饰了一个代码块,该代码块使用 "this" 作为锁对象。这意味着在同一时间只有一个线程可以访问该代码块。

面试官:很好。那么,你能解释一下 "synchronized" 关键字的实现原理吗?

候选人: 当一个线程访问一个被 "synchronized" 关键字修饰的方法或代码块时,它会尝试获取该对象的监视器锁。如果该锁已经被其他线程持有,则该线程将被阻塞,直到该锁被释放。下面是一个使用 "synchronized" 关键字的示例:

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized void decrement() {
        count--;
    }

    public synchronized int getCount() {
        return count;
    }
}

在上面的代码中,"increment()"、"decrement()" 和 "getCount()" 方法都被 "synchronized" 关键字修饰。这意味着在同一时间只有一个线程可以访问这些方法。

面试官:很好,你对 "synchronized" 关键字的理解很清晰。那么,你能告诉我 "synchronized" 关键字的缺点吗?

候选人: 当然。使用 "synchronized" 关键字会带来一些性能上的开销,因为每个线程都需要获取锁才能访问被修饰的代码。此外,如果使用不当,还可能会导致死锁等问题。

面试官:那么,你能告诉我如何避免 "synchronized" 关键字带来的性能开销吗?

候选人: 当然。一种方法是使用 "volatile" 关键字来修饰变量,这可以确保变量的可见性,而不需要使用锁。另一种方法是使用 "java.util.concurrent" 包中的并发集合类,例如 ConcurrentHashMap、CopyOnWriteArrayList 等,这些类使用了更高效的同步机制,可以避免 "synchronized" 关键字带来的性能开销。

面试官:很好,你的回答很不错。那么,你能告诉我 "synchronized" 关键字和 "Lock" 接口之间的区别吗?

候选人: 当然。"synchronized" 关键字是 Java 中内置的同步机制,它可以用来修饰方法或代码块,使用起来比较简单,但是它的性能开销比较大。而 "Lock" 接口是 Java 中提供的一种更加灵活的同步机制,它可以实现更细粒度的锁控制,例如可重入锁、读写锁等,使用起来比较复杂,但是它的性能开销比较小。

面试官:很好,你的回答很清晰。那么,你有没有使用过 "Lock" 接口呢?

候选人: 是的,我有使用过 "Lock" 接口。例如,我曾经使用过 ReentrantLock 类来实现可重入锁,这可以避免 "synchronized" 关键字的性能开销,并且可以实现更细粒度的锁控制。

面试官:很好,你的经验很丰富。那么,你能告诉我 "Lock" 接口的一些特点吗?

候选人: 当然。"Lock" 接口的一些特点包括:

  1. 可以实现更细粒度的锁控制,例如可重入锁、读写锁等。
  2. 可以实现公平锁和非公平锁。
  3. 可以实现超时锁和可中断锁。
  4. 可以实现多个条件变量,可以更加灵活地控制线程的等待和唤醒。

面试官:那么,你能告诉我 "synchronized" 关键字和 "volatile" 关键字之间的区别吗?

候选人: 当然。"synchronized" 关键字和 "volatile" 关键字都可以用来实现多线程之间的同步,但是它们的作用不同。"synchronized" 关键字可以确保在同一时间只有一个线程可以访问被修饰的代码,而 "volatile" 关键字可以确保变量的可见性,即当一个线程修改了变量的值后,其他线程可以立即看到这个修改。

面试官:看来你使用的很好,下面问一点深入的东西。回答不上来也没关系,可以自己想想。

面试官:好的,那么你能够从 "synchronized" 的底层 Java 实现角度,解释一下它的实现原理吗?

候选人: 当一个线程访问一个被 "synchronized" 关键字修饰的方法或代码块时,它会尝试获取该对象的监视器锁。如果该锁已经被其他线程持有,则该线程将被阻塞,直到该锁被释放。在 Java 中,每个对象都有一个监视器锁,也称为内部锁或互斥锁。当一个线程获取了一个对象的监视器锁后,其他线程就无法访问该对象的被 "synchronized" 关键字修饰的方法或代码块,直到该锁被释放。

在 Java 中,"synchronized" 关键字的实现是基于对象头中的标记字。当一个对象被锁定时,它的标记字会被设置为锁定状态,当锁被释放时,标记字会被清除。在 Java 6 及之前的版本中,对象头中的标记字是 32 位的,其中 25 位用于存储对象的哈希码,4 位用于存储对象的分代年龄,2 位用于存储锁标志位,1 位用于存储是否是偏向锁。在 Java 7 及之后的版本中,对象头中的标记字被重新设计,其中 32 位用于存储对象的哈希码和分代年龄,而锁标志位则被存储在一个单独的数据结构中。

面试官:很好,你的回答很详细。那么,你能够给我讲讲 "synchronized" 关键字的优化策略吗?

候选人: 当然。在 Java 中,"synchronized" 关键字的性能开销比较大,因为每个线程都需要获取锁才能访问被修饰的代码。为了优化 "synchronized" 关键字的性能,Java 6 及之后的版本中引入了偏向锁、轻量级锁和重量级锁等优化策略。

偏向锁是一种针对单线程访问同步块的优化策略。当一个线程访问一个被 "synchronized" 关键字修饰的代码块时,它会尝试获取该对象的偏向锁。如果该锁没有被其他线程持有,则该线程可以直接获取该锁,而无需进行同步操作。如果该锁已经被其他线程持有,则该线程会尝试升级为轻量级锁或重量级锁。

轻量级锁是一种针对多线程访问同步块的优化策略。当一个线程访问一个被 "synchronized" 关键字修饰的代码块时,它会尝试获取该对象的轻量级锁。如果该锁没有被其他线程持有,则该线程可以直接获取该锁,而无需进行同步操作。如果该锁已经被其他线程持有,则该线程会尝试自旋等待该锁的释放。

重量级锁是一种针对多线程访问同步块的默认策略。当一个线程访问一个被 "synchronized" 关键字修饰的代码块时,它会尝试获取该对象的重量级锁。如果该锁没有被其他线程持有,则该线程可以直接获取该锁,而无需进行同步操作。如果该锁已经被其他线程持有,则该线程会被阻塞,直到该锁被释放。

面试官:很好,你的回答很详细。那么,你能够给我讲讲 "synchronized" 关键字的底层 Java 源码实现吗?

候选人: 当然。在 Java 中,"synchronized" 关键字的底层实现是通过 monitorenter 和 monitorexit 指令来实现的。当一个线程访问一个被 "synchronized" 关键字修饰的方法或代码块时,它会尝试获取该对象的监视器锁,这可以通过 monitorenter 指令来实现。当该线程执行完被 "synchronized" 关键字修饰的方法或代码块后,它会释放该对象的监视器锁,这可以通过 monitorexit 指令来实现。


参考底层指令:

以下是 JVM 中与 "synchronized" 相关的源码:

  1. monitorenter 指令的实现:
void Interpreter::monitorenter() {
  oop obj = stack_top().get_obj(); // 获取栈顶元素,即被锁定的对象
  if (obj == NULL) { // 如果对象为空,则抛出 NullPointerException 异常
    THROW(vmSymbols::java_lang_NullPointerException());
  }
  BasicLock* lock = obj->mark()->lock(); // 获取对象的锁
  if (lock->displaced_header() == NULL) { // 如果锁没有被其他线程持有,则尝试获取锁
    // Fast path: lock is unheld, try to acquire it
    if (lock->displaced_header() == NULL &&
        lock->displaced_owner() == NULL &&
        lock->set_displaced_header()) {
      // Lock acquired
      return; // 获取锁成功,直接返回
    }
  }
  // Slow path: lock is held or contention detected
  InterpreterRuntime::monitorenter(THREAD, obj); // 获取锁失败,调用 InterpreterRuntime::monitorenter() 方法进行同步操作
}

在上面的代码中,monitorenter 指令的实现是通过获取对象的锁来实现的。如果该锁没有被其他线程持有,则该线程可以直接获取该锁,而无需进行同步操作。如果该锁已经被其他线程持有,则该线程会尝试升级为轻量级锁或重量级锁。

  1. monitorexit 指令的实现:
void Interpreter::monitorexit() {
  oop obj = stack_top().get_obj(); // 获取栈顶元素,即被锁定的对象
  if (obj == NULL) { // 如果对象为空,则抛出 NullPointerException 异常
    THROW(vmSymbols::java_lang_NullPointerException());
  }
  BasicLock* lock = obj->mark()->lock(); // 获取对象的锁
  if (lock->displaced_header() == THREAD) { // 如果锁被当前线程持有,则直接释放锁
    // Fast path: lock is held by this thread, release it
    lock->clear_displaced_header();
    return; // 释放锁成功,直接返回
  }
  // Slow path: lock is held by another thread or unheld
  InterpreterRuntime::monitorexit(THREAD, obj); // 释放锁失败,调用 InterpreterRuntime::monitorexit() 方法进行同步操作
}

在上面的代码中,monitorexit 指令的实现是通过释放对象的锁来实现的。如果该锁被当前线程持有,则该线程可以直接释放该锁,而无需进行同步操作。如果该锁被其他线程持有,则该线程会被阻塞,直到该锁被释放。

  1. ObjectMonitor 类的实现:
class ObjectMonitor : public CHeapObj<mtSynchronizer> {
  friend class VMStructs;
 private:
  volatile intptr_t _header; // 对象头,用于存储锁状态和其他信息
  volatile intptr_t _count; // 计数器,用于记录重入次数
  volatile intptr_t _waiters; // 等待队列长度,用于记录等待锁的线程数
  volatile intptr_t _recursions; // 递归深度,用于记录当前线程已经获取锁的次数
  volatile intptr_t _object; // 对象指针,指向被锁定的对象
  volatile intptr_t _owner; // 持有者指针,指向当前持有锁的线程
  volatile intptr_t _WaitSet; // 等待队列头指针,指向等待队列的头节点
  volatile intptr_t _EntryList; // 等待队列尾指针,指向等待队列的尾节点
  volatile intptr_t _cxq; // 等待队列的条件变量,用于支持条件变量的等待和唤醒操作
  volatile intptr_t _FreeNext; // 空闲链表指针,用于回收 ObjectMonitor 对象
  volatile intptr_t _Responsible; // 责任线程指针,用于记录最后一个释放锁的线程
  volatile intptr_t _SpinFreq; // 自旋频率,用于控制自旋等待的时间
  volatile intptr_t _SpinClock; // 自旋时钟,用于记录自旋等待的时间
  volatile intptr_t _SpinDuration; // 自旋持续时间,用于控制自旋等待的时间
  volatile intptr_t _SpinEarly; // 自旋提前量,用于控制自旋等待的时间
  volatile intptr_t _contentions; // 竞争次数,用于记录获取锁的竞争次数
  volatile intptr_t _succ; // 成功次数,用于记录获取锁的成功次数
  volatile intptr_t _cxqWaitTime; // 条件变量等待时间,用于记录条件变量等待的时间
  volatile intptr_t _reserved; // 保留字段,用于未来扩展
  static int _header_offset; // 对象头偏移量,用于访问对象头中的信息
  static int _count_offset; // 计数器偏移量,用于访问计数器中的信息
  static int _waiters_offset; // 等待队列长度偏移量,用于访问等待队列长度中的信息
  static int _recursions_offset; // 递归深度偏移量,用于访问递归深度中的信息
  static int _object_offset; // 对象指针偏移量,用于访问对象指针中的信息
  static int _owner_offset; // 持有者指针偏移量,用于访问持有者指针中的信息
  static int _WaitSet_offset; // 等待队列头指针偏移量,用于访问等待队列头指针中的信息
  static int _EntryList_offset; // 等待队列尾指针偏移量,用于访问等待队列尾指针中的信息
  static int _cxq_offset; // 条件变量偏移量,用于访问条件变量中的信息
  static int _FreeNext_offset; // 空闲链表指针偏移量,用于访问空闲链表指针中的信息
  static int _Responsible_offset; // 责任线程指针偏移量,用于访问责任线程指针中的信息
  static int _SpinFreq_offset; // 自旋频率偏移量,用于访问自旋频率中的信息
  static int _SpinClock_offset; // 自旋时钟偏移量,用于访问自旋时钟中的信息
  static int _SpinDuration_offset; // 自旋持续时间偏移量,用于访问自旋持续时间中的信息
  static int _SpinEarly_offset; // 自旋提前量偏移量,用于访问自旋提前量中的信息
  static int _contentions_offset; // 竞争次数偏移量,用于访问竞争次数中的信息
  static int _succ_offset; // 成功次数偏移量,用于访问成功次数中的信息
  static int _cxqWaitTime_offset; // 条件变量等待时间偏移量,用于访问条件变量等待时间中的信息
  static int _reserved_offset; // 保留字段偏移量,用于访问保留字段中的信息
  ...
};

在上面的代码中,ObjectMonitor 类是 JVM 中与 "synchronized" 相关的核心类之一。它包含了对象的监视器锁的状态信息,例如锁的持有者、等待队列、递归深度等。在 Java 中,每个对象都有一个 ObjectMonitor 对象与之对应,用于实现 "synchronized" 关键字的同步机制。

面试官:很好,你的回答很全面,你已进入候补名单。有消息会通知你。

候选人: 源码都背下来了,你给我看这。


《面试1v1》_Java

最近我在更新《面试1v1》系列文章,主要以场景化的方式,讲解我们在面试中遇到的问题,致力于让每一位工程师拿到自己心仪的offer,感兴趣可以关注公众号JavaPub追更!



🎁目录合集:

Gitee:https://gitee.com/rodert/JavaPub

GitHub:https://github.com/Rodert/JavaPub


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月13日 0

暂无评论

推荐阅读
5YI10LEk8YTp