目录

前言

一、 synchronized 的基本用法

二、 synchronized 的锁升级机制

1. 无锁状态

2. 偏向锁(Biased Locking)

3. 轻量级锁(Lightweight Locking)

4. 重量级锁(Heavyweight Locking)

5. 锁升级的完整流程

6. 锁升级的示例

7. 锁升级机制的意义

总结

synchronized 与 ReentrantLock 的对比

前言

synchronized是 Java 中用于实现线程同步的关键字,它可以确保在多线程环境下,某一时刻只有一个线程能够访问特定的代码块或方法。常见的用途包括:解决共享资源访问冲突,保证数据一致性,避免竞态条件等问题。其使用方式比较简单,但背后的实现机制较为复杂,涉及到线程锁、锁的升级等概念。

一、 synchronized 的基本用法

先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表为以下3种形式;

1 修饰普通实例方法,锁的是当前实例对象
public synchronized void method() {
    // 需要同步执行的代码
}
  • 这种方式表示对于某个实例对象(this),在同一时刻只有一个线程能够调用该实例的 method() 方法。
2 修饰静态方法,锁的是当前类的Class对象
public synchronized static void method() {
    // 需要同步执行的代码
}
  • 对于静态方法,锁是基于 Class 对象的,也就是锁住类本身。在同一时刻,只有一个线程能够执行类的静态方法。
3 修饰代码块,锁的是Synchonized括号里配置的对象
public void method() {
    synchronized (this) {
        // 需要同步执行的代码
    }
}
  • synchronized 还可以用于代码块中,指定一个对象作为锁。这里的 this 表示当前实例对象的锁,也可以使用其他对象作为锁。

二、 synchronized 的锁升级机制

synchronized 的锁升级机制是 Java 6 引入的一项重要优化,目的是在保证线程安全的同时,尽量减少锁带来的性能开销。锁升级的过程从低到高分为四个阶段:无锁状态、偏向锁、轻量级锁 和 重量级锁。

1. 无锁状态

  • 定义:对象刚被创建时,处于无锁状态。没有任何线程持有锁。

  • 特点:

    • 无锁状态是锁升级的起点。如果对象没有被任何线程访问,它会一直保持无锁状态。

2. 偏向锁(Biased Locking)

2.1 偏向锁的作用
  • 适用场景:适用于只有一个线程访问同步代码块的场景。目标是减少无竞争时的锁开销。

  • 工作原理:当第一个线程访问同步代码块时,JVM 会将对象头中的标记设置为偏向锁,并记录该线程的 ID。此时该对象的锁只偏心于该线程,因此叫偏向锁;

    • 后续如果同一个线程再次访问同步代码块,JVM 会直接允许线程进入,无需加锁。
2.2 偏向锁的升级
  • 触发条件:

    • 当另一个线程尝试获取锁时,JVM 会检查对象头的线程 ID。
    • 如果线程 ID 不匹配,说明存在竞争,偏向锁会升级为轻量级锁,同时持有该偏向锁的线程释放偏向锁。

偏向锁的撤销:JVM 会暂停持有偏向锁的线程,撤销偏向锁,并将对象头恢复为无锁状态或轻量级锁状态。

2.3 偏向锁的优点
  • 减少无竞争时的开销:在单线程访问的场景下,偏向锁几乎没有任何性能开销。

3. 轻量级锁(Lightweight Locking)

3.1 轻量级锁的作用
  • 适用场景:

    • 适用于多个线程交替访问同步代码块,但没有真正竞争的场景。
    • 目标是减少线程阻塞的开销。
  • 工作原理:

    • 当线程尝试获取锁时,JVM 会在当前线程的栈帧中创建一个锁记录(Lock Record),并将对象头中的标记复制到锁记录中。

    • 然后,JVM 会尝试通过 自旋CAS 操作将对象头中的标记替换为指向锁记录的指针。

      • 如果 自旋CAS 成功,线程获取锁。
      • 如果 自旋CAS失败,说明存在竞争,轻量级锁会升级为重量级锁
3.2 轻量级锁的升级
  • 触发条件:

    • 当存在多个线程竞争锁时,自旋CAS 操作就可能会失败。
    • 当CAS失败时,JVM 会将轻量级锁升级为重量级锁。
3.3 轻量级锁的优点
  • 减少线程阻塞:在低竞争场景下,轻量级锁通过 自旋CAS 操作避免了线程阻塞,性能较高。

4. 重量级锁(Heavyweight Locking)

4.1 重量级锁的作用
  • 适用场景:

    • 适用于高竞争场景,即多个线程同时竞争锁。目标是保证线程安全。
  • 工作原理:

    • 重量级锁依赖于操作系统的互斥信号量(Mutex)来实现线程同步。
    • 当线程尝试获取锁时,如果锁已被其他线程持有,当前线程会进入阻塞状态,等待锁释放。
4.2 重量级锁的缺点
  • 性能开销大:线程阻塞和唤醒会导致上下文切换,性能开销较大。

5. 锁升级的完整流程

  1. 初始状态:对象处于无锁状态。

  2. 偏向锁:

    • 第一个线程访问同步代码块时,JVM 将对象标记为偏向锁,并记录偏向线程的 ID。
    • 如果该线程再次访问同步代码块,JVM 会直接允许线程进入。
  3. 轻量级锁:

    • 当另一个线程尝试获取锁时,JVM 会撤销偏向锁,升级为轻量级锁。

    • 线程通过 自旋CAS 操作竞争锁。

      • 如果 CAS 成功,线程获取锁。
      • 如果 CAS 失败,说明存在竞争,升级为重量级锁。
  4. 重量级锁:

    • 当多个线程竞争锁时,轻量级锁会升级为重量级锁。
    • 线程进入阻塞状态,等待锁释放。

6. 锁升级的示例

以下是一个简单的示例,演示锁升级的过程:

public class LockUpgradeExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // 线程 1:获取锁
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread 1: 获取锁");
                try {
                    Thread.sleep(2000); // 模拟操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 线程 2:尝试获取锁
        new Thread(() -> {
            try {
                Thread.sleep(500); // 确保线程 1 先获取锁
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock) {
                System.out.println("Thread 2: 获取锁");
            }
        }).start();
    }
}

输出结果:

Thread 1: 获取锁
Thread 2: 获取锁

锁升级过程:

  1. 线程 1 获取锁,对象从无锁状态升级为偏向锁
  2. 线程 2 尝试获取锁,偏向锁升级为轻量级锁
  3. 如果线程 1 和线程 2 同时竞争锁,轻量级锁会升级为重量级锁

7. 锁升级机制的意义

  • 偏向锁:在单线程访问的场景下,偏向锁几乎没有任何性能开销。
  • 轻量级锁:在低竞争场景下,轻量级锁通过 CAS 操作避免了线程阻塞,性能较高。
  • 重量级锁:在高竞争场景下,重量级锁通过阻塞线程来保证线程安全。

总结

synchronized的锁升级机制是 Java 并发编程中的重要优化,它通过偏向锁、轻量级锁 和 重量级锁 的逐步升级,在保证线程安全的同时,尽量减少锁带来的性能开销。开发者可以根据具体的并发场景,理解锁升级的原理,从而编写出更高效的并发程序。

在这里插入图片描述

补充:synchronizedReentrantLock 的对比

特性 synchronized ReentrantLock
实现方式 JVM 内置实现 JDK 实现
锁类型 非公平锁 可选择公平锁或非公平锁
可中断性 不支持 支持
超时机制 不支持 支持
条件变量 不支持 支持
锁升级 支持(偏向锁、轻量级锁、重量级锁) 不支持
性能 低竞争场景下性能较好 高竞争场景下性能较好
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐