Java中的synchronized关键字
字数 1924 2025-11-02 13:21:23

Java中的synchronized关键字

synchronized关键字是Java中用于实现线程同步的核心机制,它可以确保多个线程在访问共享资源时的互斥性,从而避免数据不一致的问题。

1. 为什么需要synchronized?

在多线程环境中,当多个线程同时读写同一个共享资源(例如一个对象的成员变量)时,可能会发生“竞态条件”。这是因为一个简单的操作(如 count++)在底层实际上分为三个步骤:读取当前值、将值加一、写回新值。如果两个线程交错执行这三个步骤,就可能导致最终结果错误。

2. synchronized的三种应用方式

synchronized的核心理念是“锁”。一个线程在进入被synchronized保护的代码块前必须先获得锁,在退出代码块(包括正常退出和异常退出)时会自动释放锁。其他试图获取同一把锁的线程将被阻塞等待。

  • 方式一:同步实例方法

    • 作用范围:整个实例方法。
    • 锁对象:方法所属对象的实例(即 this)。
    • 代码示例
      public class Counter {
          private int count = 0;
      
          // 同步实例方法
          public synchronized void increment() {
              count++; // 此方法同一时刻只能被一个线程执行
          }
      }
      
    • 解释:假设有两个线程Thread-A和Thread-B,它们操作的是同一个Counter对象counter。当Thread-A进入increment方法时,它会获取counter对象实例的锁。此时如果Thread-B也试图调用counter.increment(),它会被阻塞,直到Thread-A执行完方法并释放锁。
  • 方式二:同步静态方法

    • 作用范围:整个静态方法。
    • 锁对象:方法所属类的Class对象(如 Counter.class)。
    • 代码示例
      public class Counter {
          private static int staticCount = 0;
      
          // 同步静态方法
          public static synchronized void staticIncrement() {
              staticCount++; // 此方法同一时刻只能被一个线程执行
          }
      }
      
    • 解释:静态方法是属于类的,而不是某个实例。因此,它的锁是类的Class对象。无论创建了多少个Counter实例,这个锁只有一把。这意味着,一个线程在执行staticIncrement时,所有其他线程都不能执行任何Counter类的同步静态方法。
  • 方式三:同步代码块

    • 作用范围:代码块{}包围的部分,更加灵活。
    • 锁对象:可以指定任意对象(synchronized (lockObject)),通常使用专门的对象来增加清晰度和控制粒度。
    • 代码示例
      public class FineGrainedCounter {
          private int count1 = 0;
          private int count2 = 0;
      
          // 创建两个不同的锁对象
          private final Object lock1 = new Object();
          private final Object lock2 = new Object();
      
          public void increment1() {
              synchronized (lock1) { // 使用lock1作为锁
                  count1++;
              }
          }
      
          public void increment2() {
              synchronized (lock2) { // 使用lock2作为锁
                  count2++;
              }
          }
      }
      
    • 解释:这种方式提供了更细粒度的锁控制。increment1increment2方法使用不同的锁(lock1lock2),因此它们可以并发执行,互不干扰。这比直接锁住整个对象(synchronized (this))或整个方法的性能更好。

3. 深入理解:锁的存储与特性

在JVM中,每个对象在堆内存中都有一个对象头,其中一部分叫做“Mark Word”,它就用来存储锁的信息。这解释了为什么Java中的任何对象都可以作为锁。

synchronized锁在JDK 1.6之后进行了重大优化,引入了“锁升级”机制,使得它的性能得到了极大提升。其升级路径是:

  • 无锁状态:刚开始,对象没有被任何线程锁定。
  • 偏向锁:如果一个线程获得了锁,锁会进入偏向模式,并记录这个线程的ID。以后该线程再次请求锁时,无需任何同步操作,直接获取,开销极小。适用于只有一个线程访问同步块的场景。
  • 轻量级锁:当有第二个线程尝试获取锁时(发生锁竞争,但竞争不激烈),偏向锁会升级为轻量级锁。线程会通过“自旋”(循环尝试)的方式等待锁释放,避免了操作系统层面昂贵的线程切换。
  • 重量级锁:如果自旋等待时间过长(竞争激烈),或者有多个线程同时竞争,轻量级锁会升级为重量级锁。此时,未能获取锁的线程会被挂起,进入阻塞状态,等待操作系统调度唤醒。这是早期synchronized效率低下的原因。

4. 总结与最佳实践

  • 作用:synchronized保证了原子性(同步块内的操作不可分割)和可见性(一个线程修改了共享变量后,其他线程能立即看到最新值)。
  • 可重入性:同一个线程可以多次获取同一把锁(例如,一个同步方法可以调用另一个同步方法),不会把自己锁死。
  • 非公平锁:synchronized是非公平锁,即等待的线程不是按先来后到的顺序获取锁,这可能导致某些线程“饥饿”。优点是吞吐量通常更高。
  • 最佳实践
    1. 减小同步范围:只同步有线程安全问题的关键代码,而不是整个方法。
    2. 使用专用锁对象:对于同步代码块,使用private final的成员变量作为锁对象,避免使用this或可被外部访问的对象,以防止死锁。
    3. 考虑并发工具包:对于复杂的并发场景(如读写锁、信号量等),java.util.concurrent包提供了更强大、更灵活的工具(如ReentrantLock),可以作为synchronized的替代或补充。
Java中的synchronized关键字 synchronized关键字是Java中用于实现线程同步的核心机制,它可以确保多个线程在访问共享资源时的互斥性,从而避免数据不一致的问题。 1. 为什么需要synchronized? 在多线程环境中,当多个线程同时读写同一个共享资源(例如一个对象的成员变量)时,可能会发生“竞态条件”。这是因为一个简单的操作(如 count++ )在底层实际上分为三个步骤:读取当前值、将值加一、写回新值。如果两个线程交错执行这三个步骤,就可能导致最终结果错误。 2. synchronized的三种应用方式 synchronized的核心理念是“锁”。一个线程在进入被synchronized保护的代码块前必须先获得锁,在退出代码块(包括正常退出和异常退出)时会自动释放锁。其他试图获取同一把锁的线程将被阻塞等待。 方式一:同步实例方法 作用范围 :整个实例方法。 锁对象 :方法所属对象的实例(即 this )。 代码示例 : 解释 :假设有两个线程Thread-A和Thread-B,它们操作的是同一个Counter对象 counter 。当Thread-A进入 increment 方法时,它会获取 counter 对象实例的锁。此时如果Thread-B也试图调用 counter.increment() ,它会被阻塞,直到Thread-A执行完方法并释放锁。 方式二:同步静态方法 作用范围 :整个静态方法。 锁对象 :方法所属类的Class对象(如 Counter.class )。 代码示例 : 解释 :静态方法是属于类的,而不是某个实例。因此,它的锁是类的Class对象。无论创建了多少个Counter实例,这个锁只有一把。这意味着,一个线程在执行 staticIncrement 时,所有其他线程都不能执行任何Counter类的同步静态方法。 方式三:同步代码块 作用范围 :代码块 {} 包围的部分,更加灵活。 锁对象 :可以指定任意对象( synchronized (lockObject) ),通常使用专门的对象来增加清晰度和控制粒度。 代码示例 : 解释 :这种方式提供了更细粒度的锁控制。 increment1 和 increment2 方法使用不同的锁( lock1 和 lock2 ),因此它们可以并发执行,互不干扰。这比直接锁住整个对象( synchronized (this) )或整个方法的性能更好。 3. 深入理解:锁的存储与特性 在JVM中,每个对象在堆内存中都有一个对象头,其中一部分叫做“Mark Word”,它就用来存储锁的信息。这解释了为什么Java中的任何对象都可以作为锁。 synchronized锁在JDK 1.6之后进行了重大优化,引入了“锁升级”机制,使得它的性能得到了极大提升。其升级路径是: 无锁状态 :刚开始,对象没有被任何线程锁定。 偏向锁 :如果一个线程获得了锁,锁会进入偏向模式,并记录这个线程的ID。以后该线程再次请求锁时,无需任何同步操作,直接获取,开销极小。适用于只有一个线程访问同步块的场景。 轻量级锁 :当有第二个线程尝试获取锁时(发生锁竞争,但竞争不激烈),偏向锁会升级为轻量级锁。线程会通过“自旋”(循环尝试)的方式等待锁释放,避免了操作系统层面昂贵的线程切换。 重量级锁 :如果自旋等待时间过长(竞争激烈),或者有多个线程同时竞争,轻量级锁会升级为重量级锁。此时,未能获取锁的线程会被挂起,进入阻塞状态,等待操作系统调度唤醒。这是早期synchronized效率低下的原因。 4. 总结与最佳实践 作用 :synchronized保证了原子性(同步块内的操作不可分割)和可见性(一个线程修改了共享变量后,其他线程能立即看到最新值)。 可重入性 :同一个线程可以多次获取同一把锁(例如,一个同步方法可以调用另一个同步方法),不会把自己锁死。 非公平锁 :synchronized是非公平锁,即等待的线程不是按先来后到的顺序获取锁,这可能导致某些线程“饥饿”。优点是吞吐量通常更高。 最佳实践 : 减小同步范围 :只同步有线程安全问题的关键代码,而不是整个方法。 使用专用锁对象 :对于同步代码块,使用 private final 的成员变量作为锁对象,避免使用 this 或可被外部访问的对象,以防止死锁。 考虑并发工具包 :对于复杂的并发场景(如读写锁、信号量等), java.util.concurrent 包提供了更强大、更灵活的工具(如 ReentrantLock ),可以作为synchronized的替代或补充。