杰瑞科技汇

synchronized锁原理是什么?如何保证线程安全?

目录

  1. 核心概念:什么是 synchronized
  2. synchronized 的三种使用方式
    • 修饰实例方法
    • 修饰静态方法
    • 修饰代码块
  3. 底层原理:synchronized 是如何工作的?
    • Java 对象头与 Monitor
    • 锁升级过程(偏向锁 -> 轻量级锁 -> 重量级锁)
  4. synchronized 的特性
    • 互斥性
    • 可见性
    • 有序性
    • 可重入性
  5. 优缺点总结
  6. java.util.concurrent.locks.Lock 的比较
  7. 最佳实践

核心概念:什么是 synchronized

synchronized 关键字可以保证在同一时刻,只有一个线程可以执行被 synchronized 修饰的代码块或方法,这种机制被称为互斥互斥锁

synchronized锁原理是什么?如何保证线程安全?-图1

它的主要目的是保证线程安全,防止多个线程同时操作共享数据时,导致数据不一致或损坏的问题。

核心思想: 当一个线程获取了某个对象的锁后,其他线程试图获取该对象的锁时,将被阻塞,直到持有锁的线程释放锁。


synchronized 的三种使用方式

a. 修饰实例方法

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

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();
        }
        System.out.println(Thread.currentThread().getName() + " finished instanceMethod.");
    }
}

工作原理:

synchronized锁原理是什么?如何保证线程安全?-图2

  • 假设有两个线程 Thread-AThread-B,它们都创建了一个 SynchronizedExample 的实例 example
  • Thread-A 调用 example.instanceMethod() 时,它会获取 example 这个对象的锁。
  • 如果 Thread-B 也想调用 example.instanceMethod(),它必须等待 Thread-A 执行完毕并释放锁。
  • 注意: Thread-B 创建了一个新的实例 example2,它可以直接调用 example2.instanceMethod(),因为它们操作的是不同的对象锁,不会互相阻塞。

b. 修饰静态方法

synchronized 修饰一个静态方法时,它锁定的是当前类的 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();
        }
        System.out.println(Thread.currentThread().getName() + " finished staticMethod.");
    }
}

工作原理:

  • 静态方法属于类,而不属于任何实例,所有实例共享同一个类级别的锁。
  • 无论有多少个 SynchronizedExample 的实例,在任何时候,都只有一个线程能执行 SynchronizedExample.staticMethod()
  • Thread-A 通过实例 example1 调用静态方法,Thread-B 通过实例 example2 调用同一个静态方法,Thread-B 必须等待 Thread-A 执行完毕。

c. 修饰代码块

这是 synchronized 最灵活、也是推荐使用的方式,你可以明确指定要锁定的对象。

public class SynchronizedExample {
    private final Object lock = new Object(); // 一个专用的锁对象
    public void blockMethod() {
        // 锁定 this 对象
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " is running in synchronized(this) block.");
        }
        // 锁定一个特定的对象
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " is running in synchronized(lock) block.");
        }
        // 锁定类的 Class 对象(效果等同于修饰静态方法)
        synchronized (SynchronizedExample.class) {
            System.out.println(Thread.currentThread().getName() + " is running in synchronized(Class) block.");
        }
    }
}

为什么推荐使用代码块?

synchronized锁原理是什么?如何保证线程安全?-图3

  • 减少锁的粒度:相比于锁定整个方法,锁定代码块可以只保护真正需要同步的代码,而方法中其他非同步代码可以并发执行,提高了性能。
  • 灵活性:可以指定任意对象作为锁,从而实现更复杂的同步逻辑,可以为不同的资源使用不同的锁,减少线程间的竞争。

底层原理:synchronized 是如何工作的?

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

a. Java 对象头与 Monitor

在 HotSpot 虚拟机中,对象在内存中分为三部分:

  1. 对象头:包含 Mark Word 和类型指针。
    • Mark Word:存储了对象的运行时数据,如哈希码、GC分代年龄、以及锁状态标志等。synchronized 的锁信息就存储在这里。
    • 类型指针:指向该对象的类元数据。
  2. 实例数据:对象真正存储的有效信息。
  3. 对齐填充:用于确保对象大小是某个字节的整数倍(8 字节)。

synchronized 依赖的是监视器锁,每个 Java 对象都可以关联一个监视器,当线程试图获取一个对象的锁时,它实际上是在尝试获取该对象关联的监视器的所有权。

  • monitorenter:JVM 指令,当线程执行到 synchronized 代码块时,会尝试获取对象的监视器,如果获取成功,计数器加 1;如果失败,线程进入阻塞状态。
  • monitorexit:JVM 指令,当线程退出 synchronized 代码块时,会释放监视器,计数器减 1,计数器为 0 时,其他线程才有机会获取该监视器。

b. 锁升级过程(JDK 1.6 优化)

为了提高性能,synchronized 的锁不是一成不变的,它会根据竞争情况在多个状态间升级,这个过程被称为偏向锁轻量级锁的优化。

  1. 无锁状态
  2. 偏向锁
    • 场景:一个锁总是被同一个线程多次获取。
    • 原理:Mark Word 中存储的是偏向线程 ID,线程获取锁时,只需进行 CAS 操作,将线程 ID 写入 Mark Word,整个过程没有阻塞,开销极小。
    • 升级:当另一个线程尝试获取这个偏向锁时,偏向锁会撤销,升级为轻量级锁。
  3. 轻量级锁
    • 场景:锁存在一定程度的竞争,但竞争不激烈。
    • 原理:竞争线程不会阻塞,而是通过自旋的方式尝试获取锁,自旋就是在一个循环中不断尝试获取锁,避免从用户态切换到内核态的开销。
    • 缺点:如果自旋时间过长(10 次左右),会消耗 CPU 资源。
    • 升级:如果自旋一定次数后仍未获取到锁,或者自旋时又有新线程来竞争,锁会升级为重量级锁。
  4. 重量级锁
    • 场景:锁竞争非常激烈。
    • 原理:未获取到锁的线程会被阻塞,并放入等待队列中,当持有锁的线程释放锁时,会通过 park() / unpark() 机制唤醒一个等待线程。
    • 缺点:线程的阻塞和唤醒需要从用户态切换到内核态,开销巨大。

锁升级路径: 偏向锁 -> 轻量级锁 -> 重量级锁,这个过程是不可逆的。


synchronized 的特性

a. 互斥性

同一时间,只有一个线程能持有锁,保证了共享资源在同一时刻只被一个线程访问。

b. 可见性

synchronized 的一个重要特性是保证内存可见性,当线程释放锁时,JMM(Java 内存模型)会强制将该线程工作内存中的所有共享变量刷新到主内存中,当线程获取锁时,JMM 会强制将该线程工作内存置为无效,从而需要从主内存中重新加载共享变量,这确保了锁的获取和释放过程对其他线程是

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