杰瑞科技汇

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

什么是 synchronized 方法?

synchronized 是 Java 中的一个关键字,用于实现线程间的同步,当一个方法被声明为 synchronized 时,它意味着:

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

在同一时间,只允许一个线程执行该方法。

如果一个线程正在执行一个对象的 synchronized 方法,那么其他所有线程(无论是尝试执行同一个对象的 synchronized 方法,还是执行该对象的另一个 synchronized 方法)都必须等待,直到第一个线程执行完毕。

这就像一个公共卫生间,门上锁了(synchronized),一个人进去后(线程进入方法),其他人只能在外面排队等待(线程阻塞),直到这个人出来(线程执行完毕),下一个人才能进去。


synchronized 方法的工作原理

synchronized 方法的实现依赖于 JVM 的内置锁(也称为监视器锁,Monitor Lock)

synchronized方法如何保证线程安全?-图2
(图片来源网络,侵删)
  1. 获取锁:当一个线程要调用一个对象的 synchronized 方法时,它必须首先获取该对象的内置锁
  2. 执行方法:如果成功获取了锁,线程就可以执行该方法体内的代码,在执行期间,它一直持有这个锁。
  3. 释放锁:当线程执行完方法体(无论是正常结束还是因为异常退出),它就会自动释放该对象的内置锁。
  4. 等待唤醒:其他正在等待获取该锁的线程,此时就有机会去获取锁,从而进入方法执行。

核心要点:锁是与对象关联的,而不是与代码或方法关联的。


语法与使用

synchronized 关键字可以放在方法返回类型之前。

1 实例方法

这是最常见的用法,锁住的是当前对象实例this)。

public class Counter {
    private int count = 0;
    // 锁住的是当前 Counter 对象实例 (this)
    public synchronized void increment() {
        count++;
        System.out.println(Thread.currentThread().getName() + " - Count: " + count);
    }
    public int getCount() {
        return count;
    }
}

工作方式

synchronized方法如何保证线程安全?-图3
(图片来源网络,侵删)
  • 假设有两个线程 Thread-AThread-B,它们都操作同一个 Counter 对象 counter
  • Thread-A 调用 counter.increment() 时,它会获取 counter 这个对象的锁。
  • 如果 Thread-B 也尝试调用 counter.increment(),它会被阻塞,因为它无法获取 counter 对象的锁,必须等待 Thread-A 释放锁。
  • 注意:Thread-B 调用的是 counter非同步方法getCount()),它不会被阻塞,因为 getCount() 不需要锁。

2 静态方法

synchronized 作用于静态方法时,锁住的是该类的 Class 对象,而不是类的某个实例。

public class SharedResource {
    // 锁住的是 SharedResource 这个类的 Class 对象
    public static synchronized void staticMethod() {
        // 代码...
    }
}

工作方式

  • 假设有两个线程 Thread-AThread-B,它们分别操作 SharedResource 的两个不同实例 resource1resource2
  • Thread-A 调用 resource1.staticMethod() 时,它会获取 SharedResource.class 这个锁。
  • 即使 Thread-B 调用 resource2.staticMethod(),它也会被阻塞,因为它也需要获取 SharedResource.class 这个锁,而这个锁已经被 Thread-A 持有了。
  • 这与实例方法形成了鲜明对比,实例方法只会在同一个实例上产生互斥。

3 代码块(更灵活的方式)

虽然问题问的是 synchronized 方法,但必须提到 synchronized 代码块,因为它更灵活、性能通常更好。

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

锁对象可以是任意对象,但必须是同一个对象才能起到同步作用。

public class Counter {
    private int count = 0;
    private final Object lock = new Object(); // 创建一个专门的锁对象
    public void increment() {
        // 只对需要同步的代码块加锁,而不是整个方法
        synchronized (lock) { // 锁住我们创建的 lock 对象
            count++;
        }
        // ... 其他不需要同步的代码
    }
}

优点

  • 粒度更细:只有真正需要保证原子性的代码才会被同步,减少了锁的持有时间,提高了并发性能。
  • 灵活性:可以指定任意对象作为锁,而局限于 thisClass 对象。

一个完整的示例

下面这个例子清晰地展示了 synchronized 方法的互斥效果。

public class SynchronizedMethodExample {
    public static void main(String[] args) {
        // 创建一个共享资源对象
        Counter counter = new Counter();
        // 创建两个线程,操作同一个 counter 对象
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        }, "Thread-A");
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        }, "Thread-B");
        // 启动线程
        thread1.start();
        thread2.start();
        // 等待两个线程执行完毕
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 最终结果应该是 2000
        System.out.println("Final count: " + counter.getCount());
    }
}
class Counter {
    private int count = 0;
    // synchronized 确保了 count++ 的原子性
    public synchronized void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}

运行结果

Thread-A - Count: 1
Thread-B - Count: 2
...
Final count: 2000

如果没有 synchronized,由于 count++ 不是原子操作(包含读取、修改、写回三个步骤),最终结果几乎肯定小于 2000。


优缺点

优点

  1. 使用简单:只需在方法前加一个关键字,JVM 会自动处理锁的获取和释放。
  2. 保证原子性:确保被同步的代码块作为一个不可分割的单元执行。
  3. 可见性:由 synchronized 保护的内存区域,当一个线程释放锁时,它在该临界区内所做的所有修改都会对后续获取该锁的线程可见。

缺点

  1. 性能开销

    • 锁获取:获取和释放锁需要额外的 CPU 时间。
    • 线程阻塞:如果一个线程长时间持有锁,其他线程就必须等待,导致上下文切换,这会带来显著的性能开销。
  2. 可能导致死锁:如果多个线程相互等待对方持有的锁,就会导致所有线程都永久阻塞,程序无法继续执行。

    // 死锁示例
    final Object lock1 = new Object();
    final Object lock2 = new Object();
    new Thread(() -> {
        synchronized (lock1) {
            System.out.println("Thread 1: Holding lock 1...");
            try { Thread.sleep(100); } catch (Exception e) {}
            System.out.println("Thread 1: Waiting for lock 2...");
            synchronized (lock2) { /* ... */ }
        }
    }).start();
    new Thread(() -> {
        synchronized (lock2) {
            System.out.println("Thread 2: Holding lock 2...");
            try { Thread.sleep(100); } catch (Exception e) {}
            System.out.println("Thread 2: Waiting for lock 1...");
            synchronized (lock1) { /* ... */ }
        }
    }).start();
  3. 不可中断:一个线程在等待获取锁时,它不能被中断(Thread.interrupt() 不会生效),只能一直等待。


总结与最佳实践

特性 synchronized 方法 synchronized 代码块
锁对象 实例方法:this
静态方法:Class 对象
任意指定的对象
粒度 粗粒度(整个方法) 细粒度(只同步代码块)
性能 较低(锁持有时间长) 较高(锁持有时间短)
灵活性
可读性 高,简单明了 中等,需要理解锁对象

最佳实践

  1. 首选 synchronized 代码块
分享:
扫描分享到社交APP
上一篇
下一篇