杰瑞科技汇

synchronized如何保证线程安全?

synchronized 是什么?

synchronized 是 Java 中的一个关键字,它主要有两种作用:

synchronized如何保证线程安全?-图1
(图片来源网络,侵删)
  1. 保证原子性:确保在同一时刻,只有一个线程可以执行被 synchronized 修饰的代码块或方法,这就像给一段代码上了一把“锁”,只有拿到钥匙的线程才能进入执行。
  2. 保证可见性:当一个线程释放锁时,JMM(Java 内存模型)会强制将该线程工作内存中的共享变量刷新回主内存,当另一个线程获取同一个锁时,JMM 会强制将该线程的工作内存置为无效,从而需要从主内存中重新加载共享变量,这样就保证了线程间的可见性。

synchronized 是 Java 内置的、最基础的锁机制,用于解决多线程环境下的竞态条件问题。


synchronized 的三种使用方式

synchronized 可以用在三个地方:实例方法、静态方法和代码块。

修饰实例方法(对象锁)

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

public class SynchronizedExample {
    public synchronized void instanceMethod() {
        // 锁定的是 this 对象
        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
(图片来源网络,侵删)
  • 锁是对象级别的,如果多个线程调用的是同一个对象instanceMethod,那么它们会互斥执行。
  • 如果线程调用的是不同对象instanceMethod,它们不会互斥,因为锁的对象不同。
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
new Thread(() -> e1.instanceMethod(), "Thread-A").start(); // 会获取 e1 的锁
new Thread(() -> e1.instanceMethod(), "Thread-B").start(); // 会阻塞,等待 e1 的锁
new Thread(() -> e2.instanceMethod(), "Thread-C").start(); // 会获取 e2 的锁,与 Thread-A 不冲突

修饰静态方法(类锁)

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

public class SynchronizedExample {
    public static synchronized void staticMethod() {
        // 锁定的是 SynchronizedExample.class 对象
        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.");
    }
}

特点

  • 锁是类级别的,无论你创建了多少个该类的实例,所有线程在调用这个静态方法时,竞争的都是同一个 Class 对象的锁。
  • 所有调用该静态方法的线程都会互斥执行。
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
new Thread(() -> e1.staticMethod(), "Thread-A").start(); // 会获取 SynchronizedExample.class 的锁
new Thread(() -> SynchronizedExample.staticMethod(), "Thread-B").start(); // 会阻塞,等待 SynchronizedExample.class 的锁
new Thread(() -> e2.staticMethod(), "Thread-C").start(); // 也会阻塞,等待同一个类锁

修饰代码块(灵活指定锁)

这是最灵活、最推荐的方式,你可以明确指定要锁定的对象,可以是任意对象,也可以是 thisClassName.class

public class SynchronizedExample {
    private final Object lock = new Object(); // 通常使用一个 final 的对象作为锁
    public void blockMethod() {
        // 锁定 this 对象
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " is running synchronized(this).");
            // ...
        }
        // 锁定类的 Class 对象
        synchronized (SynchronizedExample.class) {
            System.out.println(Thread.currentThread().getName() + " is running synchronized(ClassName.class).");
            // ...
        }
        // 锁定一个自定义的锁对象
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " is running synchronized(customLock).");
            // ...
        }
    }
}

特点

synchronized如何保证线程安全?-图3
(图片来源网络,侵删)
  • 粒度更细:你不需要锁定整个方法,只需要锁定方法中需要同步的关键代码段,减少锁的持有时间,提高并发性。
  • 灵活性高:可以选择任意对象作为锁,实现更复杂的同步逻辑。

synchronized 的底层原理(JDK 1.6 之后优化)

synchronized 的实现依赖于 JVM,在早期版本(JDK 1.6 之前),它是一种重量级锁,性能较差,但从 JDK 1.6 开始,JVM 对 synchronized 进行了大量优化,引入了锁升级机制,使其性能大幅提升。

锁的升级过程是一个不可逆的过程:

  1. 偏向锁

    • 场景:当一个锁被一个线程获取后,之后总是由这个线程来获取。
    • 原理:线程 ID 被记录在对象的 Mark Word 中,当该线程再次获取锁时,无需进行 CAS 操作加锁,只需检查 Mark Word 中的线程 ID 是否是自己即可,这是一种“无锁”状态,开销极小。
    • 适用:单线程或几乎没有竞争的场景。
  2. 轻量级锁

    • 场景:当有另一个线程尝试获取一个已经被偏向锁持有的锁时,偏向锁会升级为轻量级锁。
    • 原理:竞争线程会通过自旋 的方式尝试获取锁,自旋就是让线程空转几个循环,而不是立即阻塞,因为线程的阻塞和唤醒(从用户态到内核态的切换)是非常耗时的操作,如果在自旋期间获取到了锁,就避免了阻塞。
    • 适用:竞争不激烈,锁持有时间很短的场景。
  3. 重量级锁

    • 场景:当自旋一定次数后(默认 10 次),仍然没有获取到锁,或者有多个线程在竞争同一个锁时,轻量级锁会升级为重量级锁。
    • 原理:未获取到锁的线程会被挂起,进入阻塞状态,当持有锁的线程释放锁时,会唤醒一个阻塞的线程,这个过程涉及操作系统内核的调度,开销很大。
    • 适用:竞争激烈,锁持有时间较长的场景。

synchronized 的设计非常智能,它会根据竞争情况自动选择最合适的锁状态,从而在大多数情况下都能获得不错的性能。


synchronized 的特性与注意事项

特性

  • 可重入性:一个线程可以多次获取它已经持有的锁,这可以防止线程自己死锁自己。

    public class Reentrant {
        public synchronized void outer() {
            System.out.println("Outer method");
            inner(); // 可以再次调用同步方法
        }
        public synchronized void inner() {
            System.out.println("Inner method");
        }
    }
  • 不可中断性:一个线程在等待获取锁时,它不能被中断。threadA 正在等待 threadB 释放锁,即使你调用 threadA.interrupt()threadA 也不会被中断,它会一直等待下去,这是 synchronized 相比 Lock 接口的一个缺点。

注意事项

  • 锁对象不能为 null:如果锁对象是 null,会抛出 NullPointerException

  • 避免锁的范围过大:只同步必要的代码块,不要将整个方法都同步,尤其是包含 I/O 操作、复杂计算或网络调用的方法。

  • 不同的锁对象不互斥:一定要确保多个线程竞争的是同一个锁对象

  • 死锁:如果多个线程互相等待对方持有的锁,而没有外部干预,它们将永远等待下去。

    final Object lockA = new Object();
    final Object lockB = new Object();
    new Thread(() -> {
        synchronized (lockA) {
            try { Thread.sleep(100); } catch (Exception e) {}
            synchronized (lockB) { /* ... */ }
        }
    }).start();
    new Thread(() -> {
        synchronized (lockB) {
            try { Thread.sleep(100); } catch (Exception e) {}
            synchronized (lockA) { /* ... */ }
        }
    }).start();

synchronized vs. java.util.concurrent.locks.Lock

特性 synchronized Lock (如 ReentrantLock)
实现方式 JVM 关键字,JVM 层面实现 API,在 Java 代码层面实现
锁的获取 非阻塞,获取不到锁时会立即返回 lock() 是阻塞的;tryLock() 是非阻塞的
锁的释放 自动释放,异常时 JVM 会自动释放 必须在 finally 块中手动释放 unlock()
可中断性 不可中断 可中断(lockInterruptibly()
公平性 非公平锁 可以选择公平锁或非公平锁
条件变量 一个锁只能配一个条件(wait/notify 一个锁可以绑定多个 Condition 对象
灵活性 锁的范围只能是方法或代码块 可以实现更复杂的锁逻辑(如尝试获取、超时获取)
  • 对于绝大多数同步场景,synchronized 是简单、高效且足够好的选择。
  • 当你需要更高级的功能,如可中断、公平锁、多条件变量或超时获取锁时,才应该考虑使用 Lock 接口及其实现类(如 ReentrantLock)。

希望这份详细的解释能帮助你彻底理解 Java 的 synchronized

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