同步机制(synchronized 关键字、ReentrantLock 类)

synchronized 关键字和 ReentrantLock 类都是用来实现同步机制的,以确保多线程环境下的数据一致性和线程安全。

一、synchronized 关键字

synchronized 可以用于方法 或 方法体中的代码块

  • 当用于方法时,可使其成为同步方法
  • 当用于方法体中的代码块 时,可使其成为同步代码块

1. synchronized 用于方法

synchronized用于方法时,它会将整个方法变为同步方法。这意味着在同一时间内,只有一个线程可以执行该方法。对于实例方法,锁是当前实例对象(this);对于静态方法,锁是当前类的Class对象。

示例:同步实例方法

public class SynchronizedMethodExample {
    
    
    private int count = 0;

    // 同步实例方法
    public synchronized void increment() {
    
    
        count++;
    }

    public int getCount() {
    
    
        return count;
    }

    public static void main(String[] args) {
    
    
        SynchronizedMethodExample example = new SynchronizedMethodExample();

        // 创建多个线程来测试同步
        Thread t1 = new Thread(() -> {
    
    
            for (int i = 0; i < 1000; i++) {
    
    
                example.increment();
            }
        });

        Thread t2 = new Thread(() -> {
    
    
            for (int i = 0; i < 1000; i++) {
    
    
                example.increment();
            }
        });

        t1.start();
        t2.start();

        try {
    
    
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        // 预期输出为2000,因为increment方法是同步的
        System.out.println("Final count: " + example.getCount());
    }
}

示例:同步静态方法

public class SynchronizedStaticMethodExample {
    
    
    private static int count = 0;

    // 同步静态方法
    public static synchronized void increment() {
    
    
        count++;
    }

    public static int getCount() {
    
    
        return count;
    }

    public static void main(String[] args) {
    
    
        // 创建多个线程来测试同步
        Thread t1 = new Thread(SynchronizedStaticMethodExample::increment);
        Thread t2 = new Thread(SynchronizedStaticMethodExample::increment);

        // 启动1000次每个线程
        for (int i = 0; i < 1000; i++) {
    
    
            new Thread(SynchronizedStaticMethodExample::increment).start();
            new Thread(SynchronizedStaticMethodExample::increment).start();
        }

        // 等待所有线程完成(这里只是简单示例,实际应使用更好的线程管理方式)
        try {
    
    
            Thread.sleep(1000); // 等待足够的时间以确保所有线程完成
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        // 预期输出接近2000(可能由于线程调度而略有不同),因为increment方法是同步的
        System.out.println("Final count: " + getCount());
    }
}

2. synchronized 用于方法体中的代码块

synchronized用于方法体中的代码块时,它允许更细粒度的同步控制。您可以指定一个锁对象,并且只有持有该锁的线程才能进入并执行同步代码块内的代码。

示例

public class SynchronizedBlockExample {
    
    
    private final Object lock = new Object();
    private int count = 0;

    // 非同步方法,但包含同步代码块
    public void increment() {
    
    
        // 其他非同步代码
        // ...

        // 同步代码块
        synchronized (lock) {
    
    
            count++;
        }

        // 其他非同步代码
        // ...
    }

    public int getCount() {
    
    
        return count;
    }

    public static void main(String[] args) {
    
    
        SynchronizedBlockExample example = new SynchronizedBlockExample();

        // 创建多个线程来测试同步
        Thread t1 = new Thread(() -> {
    
    
            for (int i = 0; i < 1000; i++) {
    
    
                example.increment();
            }
        });

        Thread t2 = new Thread(() -> {
    
    
            for (int i = 0; i < 1000; i++) {
    
    
                example.increment();
            }
        });

        t1.start();
        t2.start();

        try {
    
    
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        // 预期输出为2000,因为increment方法中的同步代码块是同步的
        System.out.println("Final count: " + example.getCount());
    }
}

在这些示例中,synchronized关键字确保了increment方法的同步执行或同步代码块内的代码只能被一个线程同时访问,从而避免了并发修改导致的竞态条件。

二、ReentrantLock 类

  1. ReentrantLock 是 Java 中 java.util.concurrent.locks 包提供的一个可重入的互斥锁,它提供了与 synchronized 关键字类似的同步功能,但更加灵活和强大。
  2. ReentrantLock 提供了显式锁定和解锁的操作,以及中断正在等待获取锁的线程的能力。
  3. 在使用 ReentrantLock 时,必须确保在 finally 块中释放锁,以避免死锁的发生。
  4. 只能用于方法体中的代码块哦!!!

知识小贴士:

锁的可重入性:特指同一个线程能够多次获得同一把锁的特性,而不会引起死锁。(场景发生:递归)
互斥性:同一时间只有一个线程可以访问共享资源

示例:ReentrantLock

import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;  
  
public class ReentrantLockExample {
    
      
    private final Lock lock = new ReentrantLock();  
    private int count = 0;  
  
    // 使用ReentrantLock进行同步的方法  
    public void increment() {
    
      
        lock.lock(); // 获取锁  
        try {
    
      
            count++;  
            // 可以在这里添加其他需要同步的代码  
        } finally {
    
      
            lock.unlock(); // 确保在方法结束时释放锁  
        }  
    }  
  
    public int getCount() {
    
      
        return count;  
    }  
  
    public static void main(String[] args) {
    
      
        ReentrantLockExample example = new ReentrantLockExample();  
  
        // 创建多个线程来测试ReentrantLock的同步  
        Thread t1 = new Thread(() -> {
    
      
            for (int i = 0; i < 1000; i++) {
    
      
                example.increment();  
            }  
        });  
  
        Thread t2 = new Thread(() -> {
    
      
            for (int i = 0; i < 1000; i++) {
    
      
                example.increment();  
            }  
        });  
  
        t1.start(); // 启动线程t1  
        t2.start(); // 启动线程t2  
  
        try {
    
      
            t1.join(); // 等待线程t1完成  
            t2.join(); // 等待线程t2完成  
        } catch (InterruptedException e) {
    
      
            e.printStackTrace();  
        }   
  
        // 预期输出为2000,因为increment方法使用了ReentrantLock进行同步  
        System.out.println("Final count: " + example.getCount());  
    }  
}

三、锁的类型

1. synchronized 类锁(即静态锁)

1. 直接在静态方法上使用synchronized关键字(隐式声明)

这种方式是隐式声明类锁。当您在静态方法上使用synchronized关键字时,Java会隐式地使用该类的Class对象作为锁。

Java例子

public class MyClass {
     
     
   public static synchronized void staticMethod() {
     
     
       // 需要同步的代码
   }
}

在这个例子中,staticMethod是一个静态同步方法,它隐式地使用了MyClass.class作为锁对象。

2. 在同步块中使用ClassName.class作为锁对象(显式声明)

这种方式是显式声明类锁。您可以在同步块中直接使用类的Class对象作为锁。

Java例子

public class MyClass {
     
     
   public static void staticMethodWithBlock() {
     
     
       synchronized (MyClass.class) {
     
     
           // 需要同步的代码
       }
   }
}

在这个例子中,staticMethodWithBlock方法中的同步块显式地使用了MyClass.class作为锁对象。

3. 使用自定义的静态锁对象(显式声明)

这种方式也是显式声明类锁,但使用了一个自定义的静态对象作为锁。这种方式提供了更高的灵活性和可读性。

Java例子

public class MyClass {
     
     
   private static final Object classLock = new Object();

   public static void staticMethodWithCustomLock() {
     
     
       synchronized (classLock) {
     
     
           // 需要同步的代码
       }
   }
}

在这个例子中,classLock是一个自定义的静态锁对象,staticMethodWithCustomLock方法中的同步块显式地使用了这个锁对象。

2. synchronized 对象锁

1. 在实例方法上使用synchronized关键字(隐式声明)

public class MyObject {
     
       
   private int count = 0;  
 
   // 隐式地使用当前对象实例作为锁  
   public synchronized void increment() {
     
       
       count++;  
   }  
 
   public synchronized int getCount() {
     
       
       return count;  
   }  
}

在这个例子中,incrementgetCount方法都是同步的,它们隐式地使用了调用它们的MyObject实例作为锁。

2. 在同步块中使用对象实例作为锁对象(显式声明)

public class MyObject {
     
       
   private int count = 0;  
   private final Object lock = new Object(); // 自定义锁对象  
 
   // 显式地使用自定义锁对象作为锁  
   public void incrementWithBlock() {
     
       
       synchronized (lock) {
     
       
          count++;  
       }  
   }  
 
   public int getCountWithBlock() {
     
       
       synchronized (lock) {
     
       
           return count;  
       }  
   }  
}

在这个例子中,incrementWithBlockgetCountWithBlock方法中的同步块显式地使用了lock对象作为锁。
这种方式提供了更高的灵活性和可读性,因为您可以自定义锁对象的名称,并在需要的地方重复使用它。

3. ReentrantLock 类锁(即静态锁)

ReentrantLock 都是显式声明的

public class MyClass {
     
       
   private static final Lock classLock = new ReentrantLock();  
 
   public static void staticMethod() {
     
       
       classLock.lock();  
       try {
     
       
           // 需要同步的代码  
       } finally {
     
       
           classLock.unlock();  
       }  
   }  
}

在这个例子中,classLock是一个静态的ReentrantLock对象,它在静态方法staticMethod中被用来实现类级别的同步控制。

4. ReentrantLock 对象锁

ReentrantLock 都是显式声明的

public class MyObject {
     
       
   private int count = 0;  
   private final Lock objectLock = new ReentrantLock();  
 
   public void increment() {
     
       
       objectLock.lock();  
       try {
     
       
           count++;  
       } finally {
     
       
           objectLock.unlock();  
       }  
   }  
 
   public int getCount() {
     
       
       objectLock.lock();  
      try {
     
       
           return count;  
      } finally {
     
       
         objectLock.unlock();  
     }  
  }  
}

在这个例子中,objectLock是一个ReentrantLock对象,它在实例方法incrementgetCount中被用来实现对象级别的同步控制。

四、锁在开发过程中的选取(重点)

在Java中,当需要在循环调用的情况下保证同步性时,首先要考虑Bean的注入方式

Bean的注入方式:

  • 注解注入:如果您是通过Spring等框架使用注解(如@Autowired)来注入Bean的,那么这些Bean通常是单例的(默认情况下为(@Scope(“singleton”)))。这意味着在整个应用程序中,注入的Bean实例是唯一的。因此,当您在这些Bean的方法上使用synchronized关键字或ReentrantLock时,实际上是在对该单例对象进行同步控制。@Autowired这种情况下,使用实例级别的锁(隐式或显式)就足够了。
  • 构造器注入/new关键字:如果您是通过构造器或new关键字来创建Bean的实例,那么每次调用都会创建一个新的对象。在这种情况下,如果您需要在多个实例之间保持同步性(即类级别的同步),则需要使用类锁。类锁通常是通过在静态方法中使用synchronized关键字或静态的ReentrantLock对象来实现的。

归纳总结:
当在循环调用中需要保证同步性时,在大多数情况下,如果您正在使用Spring等框架进行依赖注入,并且Bean是单例的,那么使用实例级别的锁(隐式或显式)就足够了。
如果您需要在多个实例之间保持同步性,那么您可能需要使用类锁。

五、synchronized 与 ReentrantLock 的差异

synchronizedReentrantLock在可重入性上没有本质区别,它们的主要差异体现在锁的获取和释放方式上。

锁的获取和释放

  1. synchronized

    • 锁的获取是隐式的。当线程进入synchronized修饰的方法或代码块时,会自动获取锁。
    • 锁的释放也是隐式的。当线程退出synchronized修饰的方法或代码块时,锁会自动释放。
  2. ReentrantLock

    • 锁的获取是显式的。需要手动调用lock()方法来获取锁。
    • 锁的释放也是显式的。需要手动调用unlock()方法来释放锁。因此,在使用ReentrantLock时,通常会在finally块中释放锁,以确保在发生异常时也能正确释放锁,避免死锁。

其他差异

除了锁的获取和释放方式外,synchronizedReentrantLock还有以下差异:

  1. 锁的公平性

    • synchronized只能是非公平锁,即线程获取锁的顺序不是按照等待时间的长短来决定的。
    • ReentrantLock默认是非公平锁,但也可以设置为公平锁。通过构造函数的参数可以指定锁的公平性。当设置为公平锁时,线程会按照等待时间的长短来获取锁。
  2. 锁的等待和通知机制

    • synchronized使用Object类的wait()notify()notifyAll()方法来实现线程的等待和通知。这些方法必须在synchronized代码块或synchronized修饰的方法中调用。
    • ReentrantLock提供了Condition类来实现线程的等待和通知。Condition类提供了更灵活的线程等待和通知机制,可以避免“伪唤醒”问题。
  3. 锁的可中断性

    • synchronized不支持正在等待锁的线程被中断。线程会一直等待直到获取到锁。
    • ReentrantLock提供了lockInterruptibly()方法支持等待锁的线程被中断。如果线程在等待锁的过程中被中断,会抛出InterruptedException异常并释放当前获得的锁。
  4. 锁的超时获取

    • synchronized不支持设置超时时间来尝试获取锁。
    • ReentrantLock提供了tryLock(long timeout, TimeUnit unit)方法,可以设置超时时间来尝试获取锁。如果超时时间到了还没有获取到锁,会立即返回一个布尔值表示是否成功获取锁。