《面试1v1》synchronized

源码都背下来了,你给我看这

我是 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》系列文章,主要以场景化的方式,讲解我们在面试中遇到的问题,致力于让每一位工程师拿到自己心仪的offer,感兴趣可以关注JavaPub追更!

🎁目录合集:

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

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

http://javapub.net.cn

文章列表

📚最少必要面试题

📖知识点总结

下面是原创PDF干货版,持续更新中。

...

☕️Java基础

jdk8

📝数据结构与算法

  1. 冒泡排序就是这么容易
  2. 选择排序就是这么容易
  3. 插入排序就是这么容易
  4. 希尔排序就是这么容易
  5. 归并排序就是这么容易
  6. 快速排序就是这么容易
  7. 堆排序就是这么容易
  8. 计数排序就是这么容易
  9. 桶排序就是这么容易
  10. 基数排序就是这么容易

📣Mybatis

🔬搜索

Lucene

Elasticsearch

🎩Spring

Spring 学习路线图:
https://img-blog.csdnimg.cn/20201230220120483.png

Spring Boot

SpringBoot最新版常用案例整合,持续更新中 https://github.com/Rodert/SpringBoot-javapub

💞中间件

zookeeper

RocketMQ

Prometheus

流程引擎

💍Redis

📚Docker

📚sql

📚设计模式

🔒分布式

🌋shell

⚡️常用工具

Git

shell

linux

ffmpeg

实用工具

🌋加密

🔒GoLang

📚前端

💞区块链

🤖web实战

下载地址: github:https://github.com/Rodert/JavaPub-Web | gitee:https://gitee.com/rodert/JavaPub-Web

🚀实战面试

20212021 Java面试题系列教程

《面试1v1》Java面试八股文

《面试1v1》是我在面试中总结和推理出来的,准备在跳槽时温习回顾使用。

它采用对话的方式、口语化描述技术点,这里没有花费长篇大论的描述 API 怎么用,主要涉及到的都是高频面试题、及工作中如何使用,我还穿插了部分源码解析,因为现在面试中八股文必不可少,让文章由浅入深的更好理解。模拟了在真实面试场景中,候选人该如何回答。

迫不及待要看 面试1v1 全集怎么办? 目前在持续更新中,我一般会先更新到公众号,提催更

什么是《面试1v1》?

《面试1v1》是一个以对话形式讲解知识点的文章合集,是由 JavaPub 编写的真人1对1面试对话教程,通过真实案例编写,生动、有趣、干货满满。

为什么要写《面试1v1》这个专题?

我在后台收到很多读者的描述,说自己在面试准备过程中感觉抓不住重点,总是复习的没考、考的没复习。面试过后导致自己自信心受挫,不知道🤷‍♀️该看点什么来。

这里主要以我的经验给大家一个参照,我们如何在面试中自然的对答,不会因为紧张的忘记。如果用自己的话描述技术难题,避免背课文式的对话。

《面试1v1》有什么用?

文中大多是以实际面试中遇到的情况编写,几乎是大白话式的对话。涉及到的源码我也在对话中做了标注,方便我们查阅遗忘的知识点。

最终的目标是帮助大家更好的掌控面试,拿到心仪offer。

《面试1v1》收费吗,在哪里可以看到全集?

由 JavaPub 完全免费提供,并且持续更新中,在 wx 搜索 JavaPub 就可以直接查看全系列文章。

面试1v1 之后会出第二季吗?

会的,第二季会从大白话源码的角度出发,八股文的朋友不要错过。

【面试1v1】hashmap

【面试1v1】java注解

【面试1v1】java泛型

【面试1v1】java多线程

【面试1v1】CAS

【面试1v1】java反射

【面试1v1】动态代理

【面试1v1】javaNIO

【面试1v1】synchronized

【面试1v1】volatile

【面试1v1】线程池

【面试1v1】ThreadLocal

【面试1v1】JVM内存模型

【面试1v1】CountDownLatch-CyclicBarrier

【面试1v1】JVM类加载过程

【面试1v1】垃圾回收机制

原创电子书
链接:https://pan.baidu.com/s/1BUjGUevP00GqRw2b0HgBBA?pwd=6e67
提取码:6e67

看到这里了,点个关注呗!双击即可点赞!关注 @JavaPub

热门相关:骑士归来   法医娇宠,扑倒傲娇王爷   夫人,你马甲又掉了!   重生当学神,又又又考第一了!   战神