杰瑞科技汇

synchronized方法如何保证线程安全?

什么是 synchronized 方法?

synchronized 关键字在 Java 中用于实现互斥锁(Mutex),它是一种内置的锁机制,也称为监视器锁(Monitor Lock)。

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

当一个方法被声明为 synchronized 时,意味着: 在同一时刻,只允许一个线程执行该方法。

如果一个线程正在执行一个 synchronized 实例方法,那么其他所有线程都将被阻塞,直到该线程执行完毕并释放锁,其他线程才能有机会获取该锁并进入该方法。


synchronized 方法的两种形式

synchronized 可以用在实例方法和静态方法上,但它们锁定的对象是不同的,这是理解其行为的关键。

1 实例方法

public class MyObject {
    // synchronized 实例方法
    public synchronized void instanceMethod() {
        // 方法体...
        System.out.println(Thread.currentThread().getName() + " is running instanceMethod.");
        try {
            Thread.sleep(2000); // 模拟耗时操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 锁定的对象this,也就是当前实例对象

    synchronized方法如何保证线程安全?-图2
    (图片来源网络,侵删)
  • 工作原理:假设我们有两个线程 Thread-AThread-B,它们都操作同一个 MyObject 的实例(myObject)。

    • Thread-A 调用 myObject.instanceMethod() 时,它会尝试获取 myObject 这个对象的锁。
    • 一旦获取成功,Thread-A 进入方法体执行。
    • 如果 Thread-B 也调用 myObject.instanceMethod(),它会尝试获取同一个 myObject 对象的锁,但发现锁已经被 Thread-A 占用,Thread-B 会被阻塞,等待锁被释放。
    • 只有当 Thread-A 执行完方法并退出时,它才会自动释放 myObject 的锁,Thread-B 才能获取锁并进入方法。
  • 关键点:只有操作同一个对象实例synchronized 实例方法,它们之间才会互斥,如果两个线程操作的是两个不同的 MyObject 实例,它们可以同时各自执行自己的 instanceMethod,因为它们锁定的是不同的对象。

2 静态方法

public class MyObject {
    // synchronized 静态方法
    public static synchronized void staticMethod() {
        // 方法体...
        System.out.println(Thread.currentThread().getName() + " is running staticMethod.");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 锁定的对象:当前类的Class 对象(在 JVM 中,每个类都会有一个唯一的 Class 对象)。

  • 工作原理

    synchronized方法如何保证线程安全?-图3
    (图片来源网络,侵删)
    • Class 对象在 JVM 中是全局唯一的,无论你创建多少个该类的实例,它的 Class 对象都只有一个。
    • 当任何线程调用 MyObject.staticMethod() 时,它会尝试获取 MyObject.class 这个对象的锁。
    • 一旦获取成功,线程进入方法体执行。
    • 其他任何线程(无论是否操作了 MyObject 的实例)只要再调用 MyObject.staticMethod(),都会因为无法获取到 MyObject.class 的锁而被阻塞。
  • 关键点synchronized 静态方法锁的是类级别的锁,而不是实例级别的锁,这意味着所有对该类静态方法的调用都会互斥,与实例无关。


代码示例

让我们通过一个例子来直观感受这两种锁的区别。

public class SynchronizedMethodDemo {
    public static void main(String[] args) {
        // --- 场景1:测试 synchronized 实例方法 ---
        System.out.println("--- 场景1:测试 synchronized 实例方法 ---");
        MyInstanceObject instance = new MyInstanceObject();
        // 两个线程操作同一个实例
        new Thread(instance::instanceMethod, "Thread-A-Instance").start();
        new Thread(instance::instanceMethod, "Thread-B-Instance").start();
        try {
            Thread.sleep(3000); // 等待上面的线程执行完
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("\n--- 场景2:测试 synchronized 静态方法 ---");
        // --- 场景2:测试 synchronized 静态方法 ---
        // 两个线程操作不同的实例,但调用的是静态方法
        new Thread(MyInstanceObject::staticMethod, "Thread-A-Static").start();
        new Thread(MyInstanceObject::staticMethod, "Thread-B-Static").start();
    }
}
class MyInstanceObject {
    // synchronized 实例方法
    public synchronized void instanceMethod() {
        System.out.println(Thread.currentThread().getName() + " 开始执行实例方法,时间: " + System.currentTimeMillis());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 执行完实例方法,时间: " + System.currentTimeMillis());
    }
    // synchronized 静态方法
    public static synchronized void staticMethod() {
        System.out.println(Thread.currentThread().getName() + " 开始执行静态方法,时间: " + System.currentTimeMillis());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 执行完静态方法,时间: " + System.currentTimeMillis());
    }
}

预期输出分析:

  1. 场景1输出

    --- 场景1:测试 synchronized 实例方法 ---
    Thread-A-Instance 开始执行实例方法,时间: 167...
    // (等待2秒后)
    Thread-A-Instance 执行完实例方法,时间: 167...
    Thread-B-Instance 开始执行实例方法,时间: 167...
    // (再等待2秒后)
    Thread-B-Instance 执行完实例方法,时间: 167...

    Thread-B-Instance 必须等待 Thread-A-Instance 完全执行完毕后才能开始,证明了实例方法锁的是 this 对象。

  2. 场景2输出

    --- 场景2:测试 synchronized 静态方法 ---
    Thread-A-Static 开始执行静态方法,时间: 167...
    // (等待2秒后)
    Thread-A-Static 执行完静态方法,时间: 167...
    Thread-B-Static 开始执行静态方法,时间: 167...
    // (再等待2秒后)
    Thread-B-Static 执行完静态方法,时间: 167...

    同样,Thread-B-Static 必须等待 Thread-A-Static 完全执行完毕,即使它们操作的是不同的 MyInstanceObject 实例(在 main 方法中可以创建两个不同的实例来调用),它们依然会互斥,因为静态方法锁的是 MyInstanceObject.class


synchronized 方法的优缺点

优点

  1. 使用简单:语法非常直观,只需在方法签名前加上 synchronized 关键字。
  2. 内置锁管理:JVM 会自动处理锁的获取和释放,当一个线程进入 synchronized 方法时,它会自动获取锁;当方法正常返回或抛出异常时,它会自动释放锁,这避免了手动加锁和解锁可能带来的死锁或忘记解锁的问题。
  3. 可重入性:Java 的内置锁是可重入的,这意味着一个已经持有锁的线程,可以再次进入由同一个锁保护的代码,一个 synchronized 方法可以调用同一个对象的另一个 synchronized 方法,而不会导致自己被阻塞。

缺点

  1. 性能开销:相比于 java.util.concurrent.locks.ReentrantLocksynchronized 的性能(在 JDK 1.6 之前)通常较差,锁的获取和释放需要从用户态切换到内核态,这是一个相对耗时的操作,从 JDK 1.6 开始,JVM 对 synchronized 进行了大量优化(如偏向锁、轻量级锁、锁消除等),其性能已经得到了巨大提升,在很多场景下与 ReentrantLock 不相上下。
  2. 功能有限
    • 无法中断:一个线程在等待获取锁时,它不能被中断,它必须一直等待,直到其他线程释放锁。
    • 无法设置超时:无法像 ReentrantLock.tryLock(long time, TimeUnit unit) 那样设置一个超时时间,避免无限等待。
    • 非公平锁synchronized 是非公平锁
分享:
扫描分享到社交APP
上一篇
下一篇