源码都背下来了,你给我看这
我是 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” 接口的一些特点包括:
- 可以实现更细粒度的锁控制,例如可重入锁、读写锁等。
- 可以实现公平锁和非公平锁。
- 可以实现超时锁和可中断锁。
- 可以实现多个条件变量,可以更加灵活地控制线程的等待和唤醒。
面试官: 那么,你能告诉我 “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” 相关的源码:
- 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 指令的实现是通过获取对象的锁来实现的。如果该锁没有被其他线程持有,则该线程可以直接获取该锁,而无需进行同步操作。如果该锁已经被其他线程持有,则该线程会尝试升级为轻量级锁或重量级锁。
- 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 指令的实现是通过释放对象的锁来实现的。如果该锁被当前线程持有,则该线程可以直接释放该锁,而无需进行同步操作。如果该锁被其他线程持有,则该线程会被阻塞,直到该锁被释放。
- 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