杰瑞科技汇

synchronized锁如何保证线程安全?

目录

  1. synchronized 是什么?
  2. synchronized 的基本使用方式
    • 修饰实例方法
    • 修饰静态方法
    • 修饰代码块
  3. synchronized 的底层原理
    • 监视器锁
    • 对象头与 Mark Word
    • 锁升级过程(偏向锁 -> 轻量级锁 -> 重量级锁)
  4. synchronized 的特点
  5. synchronizedReentrantLock 的比较

synchronized 是什么?

synchronized 是 Java 中的一个关键字,它提供了内置的锁(也称为监视器锁,Monitor Lock)机制,用于解决多线程环境下的线程安全问题

synchronized锁如何保证线程安全?-图1
(图片来源网络,侵删)

当多个线程同时访问一个共享资源时,可能会引发“竞态条件”(Race Condition),导致数据不一致或程序错误。synchronized 可以确保在同一时间,只有一个线程可以执行被其修饰的代码或方法,从而保证了线程的互斥性和可见性。

synchronized 就像一个“许可证”,只有拿到许可证的线程才能进入特定代码区域,执行完毕后必须交还许可证,其他线程才能竞争进入。


synchronized 的基本使用方式

synchronized 有三种主要的使用方式,它们锁定的对象不同,因此作用域也不同。

a. 修饰实例方法

synchronized 修饰一个非静态的实例方法时,它锁定的对象是当前实例对象(this

synchronized锁如何保证线程安全?-图2
(图片来源网络,侵删)
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();
        }
    }
}
// 测试
SynchronizedExample instance = new SynchronizedExample();
new Thread(() -> instance.instanceMethod(), "Thread-A").start();
new Thread(() -> instance.instanceMethod(), "Thread-B").start();

分析:

  • 线程 A 和线程 B 调用的是同一个对象 instanceinstanceMethod()
  • 因为锁的是 this(即 instance 对象),所以当线程 A 进入方法后,线程 B 必须等待,直到线程 A 执行完毕并释放锁。
  • 输出结果: Thread-A 会先执行,2 秒后 Thread-B 才会执行,它们不会同时运行。

b. 修饰静态方法

synchronized 修饰一个静态方法时,它锁定的对象是当前类的 Class 对象(SynchronizedExample.class

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();
        }
    }
}
// 测试
new Thread(SynchronizedExample::staticMethod, "Thread-C").start();
new Thread(SynchronizedExample::staticMethod, "Thread-D").start();

分析:

  • 锁的是 SynchronizedExample.class 这个对象。
  • 无论创建多少个 SynchronizedExample 实例,它们的 Class 对象都是同一个。
  • 线程 C 和线程 D 会互斥执行,一个执行完后另一个才能执行。
  • 输出结果: Thread-C 和 Thread-D 也会串行执行,间隔约 2 秒。

c. 修饰代码块

这是最灵活的一种方式,可以明确指定要锁定的对象。

synchronized锁如何保证线程安全?-图3
(图片来源网络,侵删)

语法: synchronized (锁对象) { ... }

  • 锁定实例对象: synchronized (this) { ... },效果与修饰实例方法相同。
  • 锁定任意对象: synchronized (lockObject) { ... },可以自定义一个对象作为锁。
  • 锁定 Class 对象: synchronized (ClassName.class) { ... },效果与修饰静态方法相同。
public class SynchronizedExample {
    private final Object lock = new Object(); // 自定义锁对象
    public void blockMethod() {
        // 锁定 this 对象
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " is running synchronized block on 'this'.");
        }
        // 锁定自定义对象 lock
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " is running synchronized block on 'lock'.");
        }
        // 锁定 Class 对象
        synchronized (SynchronizedExample.class) {
            System.out.println(Thread.currentThread().getName() + " is running synchronized block on 'Class'.");
        }
    }
}

最佳实践:

  • 永远不要用字符串常量("lock")或 this 作为锁,因为字符串常量在 JVM 中具有全局唯一性,容易导致死锁或与其他代码产生意外的锁竞争。
  • 推荐使用 private final Object lock = new Object(); 这样的私有对象作为锁,可以精确控制锁的粒度,避免误操作。

synchronized 的底层原理

synchronized 的实现与 Java 对象内存布局密切相关。

a. 监视器锁

synchronized 使用的锁是监视器锁,每个 Java 对象都可以关联一个监视器,当线程试图获取一个对象的监视器锁时,它必须先成功获取该锁,才能执行同步代码块,执行完毕后,释放监视器锁。

b. 对象头与 Mark Word

在 HotSpot JVM 中,对象在内存中分为三部分:对象头、实例数据和对齐填充

  • 对象头:包含了运行时数据,如哈希码、GC 分代年龄、锁状态信息等。
  • Mark Word:是对象头的一部分,它存储了关于锁的关键信息,锁的状态就记录在 Mark Word 中。

c. 锁升级过程

为了提高性能,JVM 对 synchronized 锁做了优化,引入了锁升级的概念,锁会根据竞争情况,从低级到高级逐步升级,这个过程是不可逆的。

  1. 偏向锁

    • 场景: 一个锁总是被同一个线程多次获取。
    • 原理: Mark Word 中会记录“偏向线程 ID”,当线程再次获取锁时,无需 CAS(Compare-And-Swap)操作,只需检查 Mark Word 中的线程 ID 是否是自己的即可,开销极小。
    • 优点: 无竞争时,性能最高。
  2. 轻量级锁

    • 场景: 当有另一个线程竞争该锁时,偏向锁会撤销,升级为轻量级锁。
    • 原理: 竞争线程会通过自旋(循环尝试获取锁)的方式来获取锁,自旋不会让线程阻塞,避免了从用户态到内核态的切换开销。
    • 优点: 避免了线程阻塞和唤醒的开销,适用于竞争时间很短的场景。
    • 缺点: 如果自旋时间过长(通常超过 10 次),会消耗大量 CPU 资源。
  3. 重量级锁

    • 场景: 当自旋一定次数后仍未获取到锁,或者有多个线程竞争同一个锁时,轻量级锁会膨胀为重量级锁。
    • 原理: 未获取到锁的线程会被阻塞,并放入等待队列中,当锁被释放时,会唤醒一个等待的线程。
    • 优点: 不会消耗 CPU 进行自旋。
    • 缺点: 线程阻塞和唤醒需要从用户态切换到内核态,开销巨大。

锁升级路径是:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,这个过程是为了在无竞争或低竞争时获得高性能,在高竞争时保证系统的稳定性。


synchronized 的特点

  1. 互斥性: 保证在同一时刻,只有一个线程能进入同步代码块或方法。
  2. 可见性: synchronized 不仅保证了原子性,还保证了可见性,当一个线程释放锁时,JMM(Java Memory Model)会强制将该线程工作内存中的所有共享变量刷新到主内存中,当另一个线程获取锁时,会强制将该线程的工作内存置为无效,从而从主内存中重新加载共享变量,这确保了线程间变量的可见性。
  3. 可重入性: 一个已经获取了锁的线程,可以再次获取该锁,这避免了由自己引起的死锁。
    public class ReentrantDemo {
        public synchronized void methodA() {
            System.out.println("进入 methodA");
            methodB(); // 可以再次调用同一个对象的同步方法
        }
分享:
扫描分享到社交APP
上一篇
下一篇