杰瑞科技汇

synchronized底层实现与锁优化机制是怎样的?

目录

  1. synchronized 是什么?
  2. synchronized 的使用方式
  3. synchronized 的底层原理
  4. synchronized 的特性与优缺点
  5. synchronizedjava.util.concurrent.locks.Lock 的比较
  6. 最佳实践与总结

synchronized 是什么?

synchronized 是 Java 中用于实现线程同步的关键字,它的核心作用是确保在同一时间,只有一个线程可以执行被 synchronized 修饰的代码块或方法,从而避免多线程环境下因共享资源竞争而导致的数据不一致问题。

synchronized底层实现与锁优化机制是怎样的?-图1
(图片来源网络,侵删)

可以把它想象成一个“房间的钥匙”:

  • 当一个线程进入 synchronized 代码块时,它相当于拿到了这把钥匙,并进入房间执行代码。
  • 在这个线程执行期间,其他试图进入该房间的线程都必须在门外等待,因为钥匙被占用了。
  • 当线程执行完毕,它会交出钥匙,这样等待的线程中才能有一个拿到钥匙并进入房间。

这种机制保证了共享资源的原子性(Atomicity)、可见性(Visibility)和有序性(Ordering),是解决并发问题的基本手段。


synchronized 的使用方式

synchronized 有三种主要的使用方式:

实例方法锁(锁住当前对象实例)

public class SynchronizedExample {
    public synchronized void instanceMethod() {
        // 临界区代码
        System.out.println(Thread.currentThread().getName() + " is running instanceMethod.");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 锁对象this(当前类的对象实例)。
  • 作用范围:当一个线程调用一个对象的 synchronized 实例方法时,该对象的其他 synchronized 实例方法将被阻塞,直到前一个方法执行完毕。
  • 示例
    SynchronizedExample example = new SynchronizedExample();
    new Thread(example::instanceMethod, "Thread-A").start();
    new Thread(example::instanceMethod, "Thread-B").start();
    // 输出:
    // Thread-A is running instanceMethod.
    // (等待2秒后)
    // Thread-B is running instanceMethod.

静态方法锁(锁住类对象)

public class SynchronizedExample {
    public static synchronized void staticMethod() {
        // 临界区代码
        System.out.println(Thread.currentThread().getName() + " is running staticMethod.");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 锁对象Class 对象(SynchronizedExample.class),每个类在 JVM 中只有一个 Class 对象。
  • 作用范围:当一个线程调用一个类的 synchronized 静态方法时,该类的所有其他 synchronized 静态方法都将被阻塞,与具体实例无关。
  • 示例
    new Thread(SynchronizedExample::staticMethod, "Thread-C").start();
    new Thread(SynchronizedExample::staticMethod, "Thread-D").start();
    // 输出:
    // Thread-C is running staticMethod.
    // (等待2秒后)
    // Thread-D is running staticMethod.

代码块锁(锁住指定对象)

这是最灵活的方式,可以只锁定代码中需要同步的部分,而不是整个方法。

synchronized底层实现与锁优化机制是怎样的?-图2
(图片来源网络,侵删)
public class SynchronizedExample {
    private final Object lock = new Object(); // 一个专门的锁对象
    public void blockMethod() {
        // 锁住 this 对象
        synchronized (this) {
            // 临界区代码
            System.out.println(Thread.currentThread().getName() + " is running in 'this' synchronized block.");
        }
        // 锁住一个自定义对象(推荐)
        synchronized (lock) {
            // 临界区代码
            System.out.println(Thread.currentThread().getName() + " is running in 'lock' synchronized block.");
        }
        // 锁住类对象
        synchronized (SynchronizedExample.class) {
            // 临界区代码
            System.out.println(Thread.currentThread().getName() + " is running in 'class' synchronized block.");
        }
    }
}
  • 锁对象:可以是任意对象,包括 this、类的 Class 对象,或者一个专门创建的、不用于业务逻辑的 Object 实例。
  • 最佳实践永远不要使用字符串字面量作为锁对象,因为字符串在 Java 中是会被 JVM intern(暂存) 的,可能导致不同代码块意外地锁住了同一个字符串对象。
    • 错误示例synchronized ("my_lock") { ... } 危险!
    • 正确示例private final Object lock = new Object();

synchronized 的底层原理

synchronized 的底层实现与 JVM 版本有关,主要依赖于 Monitor(监视器) 机制。

在 Java 6 之前

  • Monitor 实现synchronized 是基于 操作系统互斥量(Mutex Lock) 实现的。
  • 工作流程
    1. 当线程尝试获取锁时,需要从用户态切换到内核态,通过操作系统 API 来获取互斥量。
    2. 如果获取失败,线程会进入阻塞状态,并会被放入一个阻塞队列中。
    3. 当锁被释放时,操作系统会从阻塞队列中唤醒一个线程,让它去竞争锁。
  • 缺点:这个“用户态到内核态的切换”成本非常高,导致 synchronized 的性能在早期版本中很差。

在 Java 6 及之后(优化升级)

为了提升性能,Java 对 synchronized 进行了大量优化,引入了锁升级(Lock Escalation)机制,一个对象在内存中的布局分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding),锁信息就存储在对象头的 Mark Word 中。

synchronized 的锁状态分为四种,随着竞争升级而变化:

  1. 无锁状态

    synchronized底层实现与锁优化机制是怎样的?-图3
    (图片来源网络,侵删)

    对象的 Mark Word 中没有记录锁信息。

  2. 偏向锁

    • 目标:消除没有竞争时的同步操作,提高单线程性能。
    • 机制:当一个线程访问同步块并获取锁时,会在 Mark Word 中记录线程ID,之后该线程再次进入同步块时,JVM 发现锁对象偏向于它,无需再进行任何同步操作。
    • 适用场景:只有一个线程会反复访问同步代码的场景。
  3. 轻量级锁

    • 目标:当有另一个线程尝试竞争锁时,避免进入阻塞状态(避免用户态/内核态切换)。
    • 机制
      • 竞争线程会尝试通过自旋(Spin)的方式获取锁。
      • 自旋就是让线程执行一个空循环,在很短的时间内不断尝试获取锁,而不是立即挂起。
      • 如果自旋成功,线程就获得了锁。
      • 如果自旋一定次数后仍未成功,说明竞争比较激烈,锁就会升级为重量级锁。
    • 适用场景:线程交替执行同步块的场景(锁竞争时间很短)。
  4. 重量级锁

    • 目标:当锁竞争非常激烈时,保证最终只有一个线程能持有锁。
    • 机制:与 Java 6 之前一样,依赖操作系统的互斥量,未获取到锁的线程会被挂起,放入阻塞队列。
    • 适用场景:锁竞争持续很长时间的场景。

锁升级路径无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

这个升级过程是单向不可逆的,锁只能升级,不能降级。


synchronized 的特性与优缺点

优点

  1. 使用简单:是 Java 语言的关键字,JVM 原生支持,语法简洁,不易出错。
  2. 自动释放锁:基于 JVM 机制,锁的获取和释放是隐式的,当一个线程执行完 synchronized 代码块或因异常退出时,JVM 会自动释放锁,避免了忘记释放锁导致死锁的问题。
  3. 保证可见性synchronized 不仅保证原子性,还保证可见性,一个线程在解锁前对共享变量的修改,对后续获取该锁的另一个线程是立即可见的。
  4. 保证有序性synchronized 内部的代码和外部的代码不会发生重排序,保证了执行的有序性。

缺点

  1. 性能开销:虽然经过大量优化,但在高并发竞争激烈的情况下,重量级锁带来的用户态/内核态切换和线程阻塞/唤醒的开销仍然很大。
  2. 功能有限:功能相对简单,无法实现一些高级的锁功能,
    • 尝试获取锁并超时synchronized 无法设置获取锁的超时时间。
    • 公平锁/非公平锁synchronized 只能实现非公平锁。
    • 多个条件变量:一个 synchronized 只有一个等待队列,而 Lock 可以有多个 Condition,实现更精确的线程唤醒。
  3. 可能导致死锁:如果多个线程以不同的顺序获取多个锁,很容易发生死锁,虽然 synchronized 本身不会导致死锁,但使用不当会。

synchronizedjava.util.concurrent.locks.Lock 的比较

特性 synchronized java.util.concurrent.locks.Lock (如 ReentrantLock)
实现机制 JVM 层面的 Monitor 机制 Java 代码层面实现的 API
锁获取/释放 自动获取和释放 必须手动 lock()unlock(),通常在 finally 块中释放
锁类型 非公平锁 可选公平锁非公平锁(构造函数参数)
等待/中断 线程会一直等待,无法响应中断 可以响应中断(lockInterruptibly()
超时获取 不支持 支持(tryLock(long time, TimeUnit unit)
条件变量 一个(隐式的) 多个 Condition 对象,可实现精确唤醒
灵活性 较低 高,可实现更复杂的同步逻辑
使用复杂度 低,简单易用 高,需要手动管理锁的生命周期,容易出错
性能 Java 6 后优化很好,在低竞争下性能优秀 在高竞争下,性能通常优于 synchronized,因为它提供了更多优化手段

如何选择?

  • 优先使用 synchronized:对于绝大多数同步场景,synchronized 已经足够好,并且更安全、更简单,除非有明确的、synchronized 无法满足的需求。
  • 考虑使用 Lock:当需要以下功能时,应考虑使用 ReentrantLock
    • 需要实现公平锁
    • 需要尝试获取锁并设置超时。
    • 需要可中断的锁获取
    • 需要多个条件变量来管理线程等待/唤醒。

最佳实践与总结

  1. 作用范围最小化:尽量使用 synchronized 代码块而不是同步整个方法,只锁定真正需要同步的代码段,以减少锁的持有时间,提高并发性。

    // 不推荐
    public synchronized void badMethod() {
        // ... 一些非同步代码 ...
        // ... 一些同步代码 ...
    }
    // 推荐
    public void goodMethod() {
        // ... 一些非同步代码 ...
        synchronized (this) {
            // ... 只同步必要的代码 ...
        }
    }
  2. 避免在 synchronized 块中调用外部方法:特别是可能被重写或执行时间不确定的方法,这会延长锁的持有时间,增加死锁风险。

  3. 使用专门的锁对象:对于实例方法,如果同步范围仅限于某个特定资源,不要使用 synchronized (this),而是创建一个 private final 的锁对象,这可以避免与其他无关代码(如第三方库)因锁 this 而产生不必要的阻塞。

    private final Object lock = new Object();
    public void doSomething() {
        synchronized (lock) {
            // ...
        }
    }
  4. 注意死锁:如果需要获取多个锁,始终以相同的顺序获取它们,先锁 A,再锁 B,这样即使多个线程也遵循这个顺序,也不会出现线程 A 拿到 A 等待 B,而线程 B 拿到 B 等待 A 的情况。

synchronized 是 Java 并发编程的基石,它通过内置锁机制简单有效地解决了线程安全问题,尽管 java.util.concurrent.locks.Lock 提供了更强大的功能和更好的灵活性,但对于绝大多数日常开发场景,synchronized 仍然是首选,因为它简单、安全且在 JVM 层面得到了深度优化,理解其原理、使用方式和优缺点,对于编写高质量、高性能的并发程序至关重要。

分享:
扫描分享到社交APP
上一篇
下一篇