你有多久没被“锁”住了?
前段时间,我一位跳槽到大厂的朋友阿光,兴冲冲地给我打电话,说他在面试的时候被一道多线程的锁升级问题卡住了。
“小米,我面试被问了个多线程的锁升级问题!你知道Java的锁还能升级吗?”
我当时一听,嘿,这不就是面试高频考点嘛!于是赶紧给他科普了一番,顺便也把这个知识点整理出来,给准备跳槽的兄弟姐妹们避避坑。
今天,我们就来深入拆解 Java的锁升级原理,搞懂它是怎么一步步优化性能的!
锁的演变:从无锁到偏向锁,再到轻量级锁和重量级锁
在JVM中,为了提高多线程的并发性能,锁并不是一开始就用最重量级的方式(比如synchronized的内置锁),而是采取了一种 “能轻则轻” 的策略。
什么策略呢?
JVM在JDK 1.6之后引入了一套锁的优化机制,让锁的状态能够动态升级,从 偏向锁 → 轻量级锁 → 重量级锁,这样就能最大程度地减少不必要的同步开销,提高性能。
我们来用一个生动的比喻来看这个过程:
你是一个公司的老板(主线程),你有一个保险柜(临界资源)
- 偏向锁: 你发现自己总是一个人在开保险柜,就不再设密码,直接认主。
- 轻量级锁: 偶尔有其他人(线程)想开柜子,你换成一把轻便的钥匙,而不是直接上大铁锁。
- 重量级锁: 太多人抢着开柜子,你无奈,只好换成一把大铁锁,并安排一个保安来管理柜子的开关(阻塞线程)。
所以,Java的锁机制就是为了尽可能减少线程竞争的开销,让锁的获取和释放更高效。
无锁状态(No Lock)
“如果能不加锁,尽量别加锁。”
在多线程环境下,如果某个对象从来没有被多个线程争抢过,JVM不会给它加锁,也就是说,它处于 无锁状态。
场景举例:
像这样的局部变量,由于不会被多个线程访问,自然不需要锁。
偏向锁(Biased Locking)
“只要一直是我一个人用,就不用管别人。”
如果一个线程在一段时间内独占某个对象,JVM会给这个对象加上偏向锁。这意味着:
- 只要没有其他线程尝试获取这个锁,锁就不会被释放或者升级。
- 获取锁的线程可以直接进入临界区,不会有额外的CAS操作,性能开销极小。
如何判断一个对象是偏向锁?
JVM会在对象的 Mark Word 里存储 持有锁的线程ID,如果下次同一个线程来获取这个锁,就可以直接进入同步代码块,不用进行额外的操作。
代码示例:
什么时候偏向锁会失效?
- 如果有其他线程来竞争这个锁,偏向锁就会升级为轻量级锁。
- 如果JVM发现锁经常被多个线程竞争,偏向锁会被禁用(可通过-XX:-UseBiasedLocking禁用)。
轻量级锁(Lightweight Locking)
“来了第二个人?那就换个稍微严谨点的加锁方式吧。”
当偏向锁失效时,JVM会把偏向锁升级为 轻量级锁。
轻量级锁的核心机制是:使用CAS(Compare-And-Swap)操作来尝试获取锁,而不是直接阻塞线程。
如果一个线程发现锁已经被其他线程持有,它不会立即阻塞,而是会 不断尝试CAS 来获取锁,这就大大减少了线程阻塞和上下文切换的开销。
示例代码:
什么时候轻量级锁会升级?
- 如果有多个线程频繁竞争锁,并且CAS失败次数过多,JVM会升级为重量级锁。
重量级锁(Heavyweight Locking)
“人太多了,排队等吧。”
如果多个线程频繁竞争一个锁,并且CAS操作不断失败,JVM会把轻量级锁升级为 重量级锁。
重量级锁的特点:
- 使用 OS级别的Mutex互斥锁,线程会被阻塞,等待唤醒。
- 线程获取锁的代价高,会产生上下文切换。
示例代码:
如何避免重量级锁?
- 尽量缩小锁的粒度,避免多个线程长时间竞争同一个锁。
- 使用并发包(如ReentrantLock) ,减少锁的冲突。
总结:锁升级流程
END
这就是 Java锁的升级原理!
希望下次面试再遇到这个问题,你可以自信地回答!如果你觉得这篇文章对你有帮助,欢迎 点赞、收藏、转发!
我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!