嘿,大家好,我是你们的小米!今天咱们来聊一个面试中常常出现的题目,尤其是Java社招面试中,几乎是必问的经典问题:“synchronized底层是如何实现的?”
相信大家在学习Java的时候,synchronized这个关键词肯定不会陌生,它是我们用来实现多线程同步的一个常见工具。但是,很多同学可能知道如何使用它,却对它的底层实现原理不甚了解。而实际上,理解它的实现机制,尤其是JVM如何管理它,能帮助我们更好地优化多线程程序,避免性能瓶颈。
所以,今天这篇文章,我就来带大家从一个非常简单的面试题入手,逐步深入,讲解synchronized的底层实现原理,希望能让你在面试中脱颖而出。
synchronized的基础概念在我们深入原理之前,先快速回顾一下synchronized的基本概念。在Java中,synchronized关键字用来确保在一个时刻只有一个线程可以执行特定的代码块。它常见的应用场景包括:
同步方法:通过在方法声明前加上synchronized,来保证同一时间内只有一个线程能调用这个方法。
同步代码块:通过在代码块中加上synchronized,我们可以控制同步的粒度,指定一个对象作为锁,只有获得该锁的线程才能执行代码块。
说到这里,大家应该已经对synchronized的基本用法了然于心了。接下来,我们将目光聚焦到它的底层实现原理。
synchronized的底层实现原理1. monitor(监视器)
首先,我们要明白的是,synchronized底层的实现依赖于JVM中的monitor。每一个对象在JVM中都有一个与之关联的monitor,用来实现对该对象的锁定和释放。当一个线程想要进入一个被synchronized修饰的代码块时,它必须首先获得该对象的monitor。
每个Java对象的monitor实际上是由对象头(Object Header)中的一部分数据结构来实现的,这部分数据结构在JVM中被称为Monitor对象。这些Monitor对象保存了线程同步所需要的信息,包括:
锁的持有者(哪个线程持有锁)
等待该锁的线程列表
锁的状态信息
通过这些信息,JVM能够知道当前哪个线程持有锁,哪个线程正在等待,进而控制线程的执行顺序。
2. monitorenter和monitorexit指令
为了理解synchronized底层的实现,我们必须要知道JVM中如何通过字节码来实现这些锁操作。JVM通过两条关键的字节码指令来处理synchronized:
monitorenter:表示线程试图获取对象的锁。
monitorexit:表示线程释放对象的锁。
这两条指令是synchronized实现的核心部分,JVM在执行这些指令时,会对对象头中的Monitor对象进行相应的操作,确保只有一个线程能进入临界区。接下来,我们通过反汇编指令的方式,来看下这些指令是如何工作的。
3. monitorenter指令
当一个线程进入被synchronized修饰的代码块时,JVM会生成一个monitorenter指令。这条指令的作用就是让线程尝试获取该对象的monitor,如果锁被其他线程占用,当前线程将进入阻塞状态,直到获取到锁为止。换句话说,monitorenter指令执行的过程是加锁操作。
以以下代码为例:
在JVM编译后,对应的字节码中会包含如下的指令:
在这里,monitorenter指令会在获取锁时阻塞当前线程,直到获得锁为止。
4. monitorexit指令
当线程执行完synchronized代码块中的代码后,它需要释放该锁。此时,JVM会生成monitorexit指令,它的作用是将该对象的monitor释放,允许其他线程获取锁。
monitorexit指令保证了只有一个线程能够执行临界区的代码,且它在执行完之后会释放锁,让其他线程可以继续执行。
synchronized的加锁过程通过上述反汇编代码,我们可以看到synchronized的加锁过程是通过monitorenter指令来完成的。下面,我将简单地总结一下加锁的流程:
线程到达synchronized关键字修饰的代码块时,JVM生成monitorenter指令,尝试获取对象的锁。
如果该锁被其他线程持有,当前线程会进入等待状态,直到锁被释放。
如果锁未被占用,线程将获取该锁并进入临界区代码执行。
执行完毕后,线程通过monitorexit指令释放锁。
synchronized的解锁过程与加锁类似,synchronized的解锁是通过monitorexit指令来实现的。当线程执行完synchronized代码块后,JVM会执行monitorexit,释放锁。此时,其他等待该锁的线程将有机会获取锁,进入临界区执行。
synchronized的优化Java的synchronized并不是一成不变的,在JVM的实现中,它经过了多次优化,尤其是在JDK 1.6及之后的版本。为了提高并发性能,JVM引入了偏向锁、轻量级锁、重量级锁等优化策略。
1. 偏向锁(Biased Locking)
偏向锁是JVM在JDK 1.6中引入的一种优化机制,它的目的是减少多线程竞争时的加锁开销。当一个线程获得锁时,JVM会偏向这个线程,即使其他线程请求该锁,也不会立即进行锁的竞争。偏向锁的释放条件是锁的拥有者线程退出后,才会重新考虑锁的竞争。
2. 轻量级锁(Lightweight Locking)
当多个线程竞争同一个对象时,JVM会在加锁时使用轻量级锁,这比传统的互斥锁更高效。轻量级锁的机制是,通过在对象头中存储指针来指向锁的拥有者线程。只有当锁被竞争时,才会升级为重量级锁。
3. 重量级锁(Heavyweight Locking)
当多个线程在竞争锁时,JVM会将轻量级锁升级为重量级锁,此时会引入操作系统层面的锁。重量级锁是最为传统的锁机制,但它会导致线程的上下文切换,性能开销较大,因此JVM会尽量避免使用。
总结通过今天的学习,我们不仅了解了synchronized的基本概念,还深入探讨了它的底层实现原理。我们从monitor对象的概念入手,分析了JVM如何通过monitorenter和monitorexit字节码指令来实现加锁和解锁操作。
此外,我们还讲解了synchronized的一些优化策略,比如偏向锁、轻量级锁和重量级锁,这些优化大大提高了Java程序在多线程环境下的执行效率。
END理解这些底层原理,能让我们更好地运用synchronized,也能帮助我们在面试中展现自己的深厚技术功底。如果你正在准备Java社招面试,这个问题一定不要错过哦!
希望今天的文章对你有所帮助,我们下次再见!
熬夜码字不易,一杯奶茶续命!看完文章别忘了顺手点开图片广告,让作者攒点奶茶基金,感激不尽!
我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!