故事引入
在一个古老而神秘的城市中,有一个充满历史韵味的档案馆。这座档案馆珍藏着城市的记忆与秘密,吸引着众多研究者前来探寻。档案馆的馆长,一位智慧且充满人情味的老人,负责维护档案馆的秩序与安全。
档案馆有个规矩,那就是每次只能有一个人进入操作档案,以确保档案的完整与安全。起初,档案馆的访客并不多,馆长总是能轻松应对。每当有熟悉的研究者来访,馆长会亲切地说:“我认识你,你来过,不用登记了,直接进去吧。”(偏向锁)这样,研究者们就能省去繁琐的登记手续,直接开始他们的研究工作。
然而,随着时间的推移,档案馆的名气越来越大,越来越多的研究者开始涌入。馆长意识到,单纯依靠自己的记忆已经无法满足需求了。于是,他决定在一个合适的时间闭馆(全局安全点),搞一个先进的登记系统(轻量级锁)。
这个登记系统非常神奇,它能够自动记录每个研究者的到访信息,并平等地分配进入档案馆的机会。当两位研究者同时想要进入时,登记系统会亮起绿灯,表示现在可以进行竞争。馆长会站在一旁,公正地看着每一位研究者,不再偏向任何一方。(非公平锁)
“现在有两个人都想进入,你们就相互竞争吧。”馆长笑着说,“登记后等待,没竞争到的人也别灰心,等等看,也许下一位就是你。”
研究者们按照登记系统的指示,依次进行登记并等待。登记系统还设定了一个默认的等待时间,通常是十分钟(默认自旋十次)。一些经常来的研究者们,他们心里对于排队等待时间早有了预期,于是,他们便会根据以往的等待成功率,来抉择自己这次要等待多久。(自适应自旋)这种方式在保证了他们有了足够的等待成功率,也不至于浪费过多的时间。
然而,有一天,档案馆迎来了前所未有的热潮。门口排起了长长的队伍,人数众多,甚至连登记系统都快要应接不暇了。这时,馆长决定启动重量级锁机制来应对。
“现在人数太多了,我们需要采取更严格的措施。”馆长严肃地说,“你们可以选择等待一段时间,但如果在预期时间内无法进入,就请先离开吧。等里面的人出来了,我会发短信通知你们再来。”
研究者们听了馆长的话,有的选择继续等待,有的则因为时间紧迫而选择了离开(线程挂起)。而那些离开的研究者们,在收到馆长的通知(线程唤醒)后,会再次前来档案馆,继续他们的研究之旅。
通过偏向锁、轻量级锁和重量级锁的灵活运用,档案馆的秩序得到了有效维护。研究者们也能够根据不同的情况选择最适合自己的访问方式,既保证了数据的安全性,又提高了访问的效率。
(图片来源于网上)
原理分析
偏向锁
偏向锁的设计初衷是优化无竞争情况下的性能。当一个线程首次访问同步代码块并获取锁时,会以 CAS 尝试将 Markword 的偏向线程 ID 修改为自己的线程 ID 。如果修改成功,那在接下来的访问中,只要这个线程再次访问该同步代码块,JVM 会认为它仍然应该拥有这个锁,从而直接允许它进入,无需再次进行锁的竞争,减少了不必要的开销。
随着系统中线程数量的增加,对同一个锁的竞争也会逐渐增多。当其他线程尝试获取已经被偏向的锁时,JVM 会检查当前锁的偏向状态。如果发现有多个线程竞争同一个偏向锁,那么偏向锁就不再适用,需要进行升级。
轻量级锁
在偏向锁升级为轻量级锁的过程中,全局安全点起到了关键作用。全局安全点是 JVM 的一个特定位置,它允许 JVM 安全地暂停所有线程以进行某些操作,如垃圾回收或锁升级。在这个安全点上,JVM 会暂停持有偏向锁的线程,并检查锁的状态。
轻量级锁的设计是为了处理短时间内锁竞争的情况。当锁从偏向锁升级为轻量级锁时,JVM 会尝试通过自旋的方式让当前线程等待锁的释放。自旋意味着线程不会立即阻塞,而是会在循环中等待一段时间,看锁是否会被释放。这种方式适用于短时间内锁会被释放的场景,可以避免线程阻塞和上下文切换的开销。
自适应自旋
自旋是指当线程尝试获取锁时,如果锁被其他线程持有,该线程不会立即阻塞,而是会在一个循环中等待一段时间,这段时间内线程会执行一个空循环(什么也不做),即所谓的“自旋”。自旋的目的是为了在短时间内尝试获取锁,以避免线程阻塞和上下文切换的开销。
然而,简单的自旋策略存在一个问题:就像故事中的研究者们,如果等待的人过多,那么很可能需要浪费很多时间在等待上,这也许是不太划算的。因此,如果锁被持有的时间很长,或者多个线程都在进行自旋,那么自旋的线程会浪费大量的 CPU 时间。为了解决这个问题,在 JDK1.6 中 Java 引入了自适应自旋的概念。
自适应自旋根据线程之前自旋获取锁的成功率来进行调整。如果之前在该锁上的自旋成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
自适应自旋十分巧妙,是 synchronized 锁升级原理中的一个重要优化手段,它通过根据实际的锁竞争情况动态调整自旋策略,提高了轻量级锁的性能和响应速度。
重量级锁
如果线程在自旋过程中长时间未能获取到锁,或者自旋超过了次数,那么 JVM 会认为当前的锁竞争较为激烈,轻量级锁已经不再适用。此时,锁会进一步升级为重量级锁。
重量级锁是 Java 中最传统的锁实现方式,它依赖于操作系统的互斥量(Mutex)来实现。当锁升级为重量级锁时,线程会阻塞并等待操作系统的调度,直到获得锁为止。这种方式虽然能够确保线程安全地访问共享资源,但会引入较大的性能开销,包括线程阻塞、上下文切换等。
(图片来源于网上)
synchronized 简单 Demo
public class SynchronizedDemo {
private Object lock = new Object(); // 定义一个对象作为锁
public void method1() {
synchronized (lock) {
System.out.println("线程" + Thread.currentThread().getId() + "执行method1");
try {
Thread.sleep(1000); // 模拟方法执行耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method2() {
synchronized (lock) {
System.out.println("线程" + Thread.currentThread().getId() + "执行method2");
try {
Thread.sleep(1000); // 模拟方法执行耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
// 创建五个线程来模拟抢占锁的过程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronizedDemo.method1();
synchronizedDemo.method2();
}).start();
}
}
}
运行结果:
线程12执行method1
线程16执行method1
线程16执行method2
线程15执行method1
线程15执行method2
线程14执行method1
线程13执行method1
线程13执行method2
线程14执行method2
线程12执行method2
最后
总结来说,synchronized 锁的升级原理是一个根据线程竞争情况动态调整锁策略的过程。从偏向锁到轻量级锁再到重量级锁,JVM根据当前系统的线程竞争情况和性能需求来选择合适的锁策略,以平衡线程安全和性能开销。这种动态调整的特性使得 synchronized 锁能够在不同的场景下都能表现出良好的性能。今天的故事到这里就结束啦,如果有所收获请点赞支持一下作者哦,您的点赞是我持续创作的动力!!