前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >深入剖析 Java 的 synchronized 锁升级过程

深入剖析 Java 的 synchronized 锁升级过程

原创
作者头像
小马哥学JAVA
发布2025-01-19 14:10:22
发布2025-01-19 14:10:22
25600
代码可运行
举报
运行总次数:0
代码可运行

前言

在 Java 并发编程领域,synchronized关键字堪称保障线程安全的中流砥柱。随着 JDK 版本的迭代演进,synchronized锁的性能优化也在持续推进,其中锁升级机制尤为关键。本文将深度剖析synchronized锁从偏向锁、轻量级锁到重量级锁的升级历程,详细阐述每个状态的含义、适用场景、实现方式、与 JDK 版本的关联,同时对用户态与内核态这两个重要概念进行深入解读。

一、用户态与内核态

在探讨锁升级之前,明晰用户态与内核态这两个概念至关重要。

1.1 用户态

用户态是应用程序的运行环境,处于操作系统提供的用户空间中。在此状态下,应用程序的资源访问受限,无法直接触碰硬件设备以及操作系统的核心数据结构。用户态下执行的是用户编写的代码,操作系统通过保护与限制机制,防止应用程序对系统造成破坏。

1.2 内核态

内核态是操作系统内核的运行状态,拥有至高无上的权限,能够访问系统的所有资源,涵盖硬件设备、内存等。当应用程序需要执行特权操作,比如访问硬件设备、进行进程间通信时,必须借助系统调用从用户态切换至内核态,由内核来执行这些操作。

1.3 用户态与内核态的切换

用户态与内核态的切换并非无本万利,这一过程需要保存并恢复当前进程的上下文信息,其中包括寄存器的值、程序计数器等。频繁进行这种切换会严重影响性能。在 Java 的锁机制中,减少此类切换次数成为提升性能的核心要点之一。

二、偏向锁

2.1 偏向锁的含义

偏向锁是 JDK 6 引入的一项优化创举,旨在解决仅有一个线程频繁访问同步块时的锁竞争开销问题。其核心思路是,当一个线程首次访问同步块并获取锁时,会在对象头中记录该线程的 ID。此后,该线程再次访问此同步块,无需再进行常规的锁获取操作,仅需检查对象头中的线程 ID 是否与自身一致。若一致,便能直接进入同步块,从而避免了诸如 CAS(Compare and Swap)操作等高开销的锁获取流程。

2.2 业务场景

偏向锁适用于多数时间仅有一个线程访问同步资源的场景。例如在单线程环境中,或者某个线程长时间持有锁的情况。以 Web 应用为例,某些单例对象的初始化和访问操作,若仅在应用启动时由一个线程执行,后续极少被其他线程访问,此时使用偏向锁能够显著提升这部分代码的执行效率。

2.3 实现方式

在 Java 中,对象头是实现偏向锁的关键所在。对象头包含若干标志位以及线程 ID 等信息。当一个线程首次访问同步块并成功获取锁时,JVM 会将对象头中的偏向锁标志位置为 1,并记录当前线程的 ID。后续该线程再次访问同步块时,JVM 会检查对象头中的偏向锁标志位和线程 ID,若两者匹配,线程即可直接进入同步块。

当有其他线程试图访问被偏向锁锁定的对象时,偏向锁会被撤销。在撤销过程中,JVM 会遍历持有该对象的线程栈,检查是否存在该对象的锁记录。若存在,则将偏向锁升级为轻量级锁。

2.4 模拟示例

代码语言:javascript
代码运行次数:0
复制
public class BiasedLockExample {
    private static final Object lock = new Object();
    public static void main(String[] args) {
        // 预热,促使JVM有机会启用偏向锁
        for (int i = 0; i < 10000; i++) {
            synchronized (lock) {
                // 可执行一些简单操作
            }
        }
        Thread thread = new Thread(() -> {
            long startTime = System.currentTimeMillis();
            for (int i = 0; i < 1000000; i++) {
                synchronized (lock) {
                    // 可执行一些简单操作
                }
            }
            long endTime = System.currentTimeMillis();
            System.out.println("Thread cost time: " + (endTime - startTime) + "ms");
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,我们先通过多次循环访问同步块进行预热,为 JVM 启用偏向锁创造条件。随后启动一个线程,在线程中大量访问同步块,通过计算时间来观察偏向锁对性能的影响。

2.5 底层原理

偏向锁的底层实现依赖于对象头中的 Mark Word。Mark Word 是对象头的一部分,用于存储对象的哈希码、锁状态、偏向锁标识等信息。在偏向锁模式下,Mark Word 存储了偏向锁的线程 ID 和偏向锁标志位。当线程访问同步块时,首先检查 Mark Word 中的偏向锁标志位。若标志位为 1 且线程 ID 与当前线程 ID 一致,则可直接进入同步块,无需进行任何锁获取操作。

当有其他线程尝试访问同步块时,偏向锁的撤销步骤如下:

  1. 首先,JVM 暂停拥有偏向锁的线程。
  2. 接着,遍历该线程的栈,查找是否存在该对象的锁记录。
  3. 若存在锁记录,将偏向锁升级为轻量级锁,并清除 Mark Word 中的偏向锁标志位。
  4. 最后,恢复被暂停的线程。

三、轻量级锁

3.1 轻量级锁的含义

轻量级锁是继偏向锁之后引入的锁优化机制,主要用于解决多线程竞争不太激烈场景下的锁性能问题。当多个线程在短时间内交替访问同步块时,偏向锁失效,此时轻量级锁通过 CAS 操作避免了重量级锁带来的用户态与内核态切换的高昂开销。

3.2 业务场景

轻量级锁适用于多线程竞争相对缓和的场景。例如在高并发的 Web 应用中,某些资源可能被多个线程交替访问,但竞争并不激烈。在此种情况下,使用轻量级锁能够规避重量级锁的高开销,提升系统的并发性能。

3.3 实现方式

当一个线程尝试获取轻量级锁时,JVM 会在当前线程的栈帧中创建一个锁记录(Lock Record),并将对象头中的 Mark Word 复制到锁记录中。随后,JVM 尝试使用 CAS 操作将对象头中的 Mark Word 替换为指向锁记录的指针。若 CAS 操作成功,表明该线程成功获取轻量级锁,此时对象头中的 Mark Word 变为指向锁记录的指针,且锁记录中的 owner 字段指向当前线程。

若 CAS 操作失败,意味着已有其他线程获取了该锁。此时,当前线程会尝试自旋(Spin)一定次数,再次尝试获取锁。自旋是指线程在不放弃 CPU 使用权的前提下,不断尝试获取锁,期望在短时间内其他线程能够释放锁。若自旋一定次数后仍无法获取锁,轻量级锁将升级为重量级锁。

3.4 模拟示例

代码语言:javascript
代码运行次数:0
复制
public class LightweightLockExample {
    private static final Object lock = new Object();
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                synchronized (lock) {
                    // 可执行一些简单操作
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                synchronized (lock) {
                    // 可执行一些简单操作
                }
            }
        });
        long startTime = System.currentTimeMillis();
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Two threads cost time: " + (endTime - startTime) + "ms");
    }
}

在这个示例中,我们启动两个线程同时访问同步块,模拟多线程竞争不太激烈的场景。通过观察两个线程的总执行时间,对比轻量级锁在此场景下的性能表现。

3.5 底层原理

轻量级锁的底层实现主要依赖于 CAS 操作和自旋机制。在获取轻量级锁时,通过 CAS 操作将对象头中的 Mark Word 替换为指向锁记录的指针。若 CAS 操作失败,说明锁已被其他线程持有。此时,当前线程进行自旋操作,在自旋过程中不断尝试使用 CAS 操作获取锁。

自旋的目的是在短时间内等待其他线程释放锁,避免直接进入重量级锁带来的高开销。自旋次数通常有限,不同的 JVM 实现中,自旋次数的默认值可能有所差异。若自旋一定次数后仍无法获取锁,JVM 会判定竞争较为激烈,进而将轻量级锁升级为重量级锁。

自旋锁什么时候升级成重量级锁

JDK1.6之前:在某一个线程自旋次数超过十次就会升级成重量级锁

JDK1.6之后:自适应自旋,JDK根据线程运行情况自己判断

四、重量级锁

4.1 重量级锁的含义

重量级锁是传统的synchronized锁实现方式,通过操作系统的互斥量(Mutex)实现线程同步。在重量级锁模式下,当一个线程尝试获取锁时,若锁已被其他线程持有,当前线程会被阻塞,进入等待队列,等待操作系统的调度。这种方式会导致用户态与内核态的频繁切换,从而产生较高的性能开销。

4.2 业务场景

重量级锁适用于多线程竞争极为激烈的场景。例如在需要大量并发访问共享资源的场景中,当轻量级锁和偏向锁无法满足需求时,便会升级为重量级锁。以大型分布式系统为例,多个节点同时访问共享的数据库资源,竞争激烈,此时可能会采用重量级锁来确保数据的一致性。

4.3 实现方式

在 Java 中,当轻量级锁升级为重量级锁时,JVM 会在对象头中设置一个指向重量级锁(Monitor)的指针。Monitor 是操作系统提供的同步机制,包含一个等待队列和一个入口队列。当一个线程尝试获取锁时,若锁已被其他线程持有,该线程会被放入 Monitor 的等待队列中,进入阻塞状态。当持有锁的线程释放锁时,会从等待队列中唤醒一个线程,使其有机会获取锁。

4.4 模拟示例

代码语言:javascript
代码运行次数:0
复制
public class HeavyweightLockExample {
    private static final Object lock = new Object();
    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000000; j++) {
                    synchronized (lock) {
                        // 可执行一些简单操作
                    }
                }
            });
        }
        long startTime = System.currentTimeMillis();
        for (Thread thread : threads) {
            thread.start();
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Ten threads cost time: " + (endTime - startTime) + "ms");
    }
}

在这个示例中,我们启动 10 个线程同时访问同步块,模拟多线程竞争非常激烈的场景。通过观察 10 个线程的总执行时间,了解重量级锁在此场景下的性能表现。

4.5 底层原理

重量级锁的底层实现依赖于操作系统的互斥量(Mutex)。当一个线程获取重量级锁时,需通过系统调用进入内核态,请求操作系统分配一个互斥量。若互斥量已被其他线程持有,当前线程会被放入等待队列,进入阻塞状态,此时线程从用户态切换至内核态。

当持有锁的线程释放锁时,会唤醒等待队列中的一个线程。被唤醒的线程会从内核态切换回用户态,尝试获取锁。这种用户态与内核态的频繁切换会带来较高的性能开销,因此在竞争不激烈的情况下,应尽量避免使用重量级锁。

五、锁升级过程总结

在 Java 中,synchronized锁的升级过程是一个逐步优化的过程。当一个线程首次访问同步块时,JVM 会尝试使用偏向锁提升性能。若有其他线程尝试访问被偏向锁锁定的对象,偏向锁会升级为轻量级锁。当多个线程竞争激烈,轻量级锁的自旋无法在短时间内获取锁时,轻量级锁会升级为重量级锁。

这种锁升级机制能够依据不同的业务场景和线程竞争状况,自动选择最为合适的锁类型,从而有效提升系统的并发性能。在实际开发中,我们应尽量避免不必要的锁竞争,合理运用synchronized关键字,充分发挥锁升级机制的优势。

六、JDK 版本与锁升级

锁升级机制在不同的 JDK 版本中存在差异。在 JDK 6 之前,synchronized锁仅有一种重量级锁的实现方式,性能相对欠佳。从 JDK 6 开始,引入了偏向锁和轻量级锁,极大地提升了synchronized锁的性能。

在后续的 JDK 版本中,锁升级机制持续优化改进。例如,在部分 JDK 版本中,对自旋策略进行了调整,根据不同的硬件环境和线程竞争情况,动态调整自旋次数,进一步提升性能。

在使用synchronized锁时,我们务必关注所使用的 JDK 版本特性,以便更好地利用锁升级机制优化程序性能。

七、总结

本文深度剖析了 Java 中synchronized锁的升级过程,涵盖偏向锁、轻量级锁和重量级锁的含义、业务场景、实现方式、模拟示例以及底层原理。同时,介绍了用户态与内核态的概念及其在锁机制中的作用。通过深入了解锁升级过程,我们能够编写出更为高效的并发程序,提升系统的性能与稳定性。在实际开发中,应依据具体的业务场景和线程竞争情况,合理选择锁的类型,充分发挥 Java 并发编程的优势。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、用户态与内核态
    • 1.1 用户态
    • 1.2 内核态
    • 1.3 用户态与内核态的切换
  • 二、偏向锁
    • 2.1 偏向锁的含义
    • 2.2 业务场景
    • 2.3 实现方式
    • 2.4 模拟示例
    • 2.5 底层原理
  • 三、轻量级锁
    • 3.1 轻量级锁的含义
    • 3.2 业务场景
    • 3.3 实现方式
    • 3.4 模拟示例
    • 3.5 底层原理
  • 四、重量级锁
    • 4.1 重量级锁的含义
    • 4.2 业务场景
    • 4.3 实现方式
    • 4.4 模拟示例
    • 4.5 底层原理
  • 五、锁升级过程总结
  • 六、JDK 版本与锁升级
  • 七、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档