synchronized关键字与ReentrantLock的区别和应用

简介: synchronized关键字与ReentrantLock的区别和应用

synchronized关键字与ReentrantLock的区别和应用

简介

你在一个咖啡店里,有一台唯一的咖啡机,顾客们需要排队使用这台咖啡机。这台咖啡机就像是一个共享资源,而synchronized关键字和ReentrantLock都是确保顾客能有序使用咖啡机的机制。

synchronized关键字:

想象synchronized就像是咖啡店的一个规矩:当一个顾客正在使用咖啡机时,其他顾客必须等待。这个规矩是由咖啡店自动强制执行的,顾客们不需要额外做什么,只需等待前面的人用完。当顾客开始使用咖啡机时,他们就自动获得了使用权,完成后也会自动释放,让下一个顾客使用。这个过程很简单,但顾客们不能决定等待多久,也不能尝试中途放弃等待。

ReentrantLock:

现在想象ReentrantLock是咖啡店提供的一个可选服务,它更像是一个高级的取号系统。当顾客进入咖啡店时,他们可以选择拿一个号码牌,这样他们就知道轮到自己的顺序了。使用ReentrantLock,顾客可以决定他们是否愿意等待(可以设置尝试获取锁的时间),或者在等待太久后选择放弃并离开咖啡店。此外,这个取号系统还允许顾客在等待时做一些其他事情(比如读书或使用手机),这就是ReentrantLock的可中断锁定特性。ReentrantLock还允许顾客按照一些公平的规则排队,比如“先来后到”,但这可能会稍微减慢整个流程。

区别和应用:

  • 简易性 vs. 灵活性:synchronized是嵌入在Java语言中的,使用起来非常简单,不需要程序员做太多的管理工作。相比之下,ReentrantLock提供了更多的灵活性,比如可中断的锁获取、定时锁等待和公平锁选项。
  • 自动释放锁 vs. 手动释放锁:使用synchronized块或方法时,JVM会自动管理锁的获取和释放。而使用ReentrantLock时,程序员必须手动调用.lock()来获取锁,并在finally块中调用.unlock()来释放锁,这样可以避免潜在的锁泄漏。
  • 条件变量:ReentrantLock还提供了条件变量(Condition),这相当于咖啡店中的额外通知机制,允许顾客在特定条件下等待或接收通知,这在synchronized中是不可用的。

在选择使用synchronized还是ReentrantLock时,如果你需要简单的同步机制,不需要额外的特性,那么synchronized是一个很好的选择。如果你需要更高级的功能,比如锁的公平性、可中断的锁等待,或者条件变量,那么ReentrantLock可能是更合适的选择。

代码演示

案例一:使用synchronized关键字的银行账户转账

假设我们有一个银行账户类,需要确保在进行转账操作时不会出现并发问题。我们使用synchronized关键字来同步访问共享资源,即账户余额。

public class BankAccount {
    private double balance; // 账户余额
    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }
    // 同步方法,保证同时只有一个线程可以执行该方法
    public synchronized void deposit(double amount) {
        balance += amount;
    }
    // 同步方法,保证同时只有一个线程可以执行该方法
    public synchronized void withdraw(double amount) {
        balance -= amount;
    }
    // 转账方法也是同步的,防止并发问题
    public synchronized void transfer(BankAccount toAccount, double amount) {
        this.withdraw(amount); // 从当前账户扣除金额
        toAccount.deposit(amount); // 向目标账户存入金额
    }
    // 获取账户余额,同步方法保护余额的读取
    public synchronized double getBalance() {
        return balance;
    }
}

在这个案例中,我们使用synchronized修饰符来确保deposit、withdraw、transfer和getBalance方法在执行时,同一时刻只有一个线程能够访问该对象的这些同步方法。

当一个线程调用BankAccount对象的方法时,如deposit、withdraw、transfer或getBalance,以下是代码的运行过程:

  1. 同步方法调用:
  • 当线程调用deposit、withdraw、transfer或getBalance方法时,由于这些方法都使用了synchronized修饰符,同一时刻只有一个线程可以执行这些方法。其他线程需要等待当前线程执行完毕后才能进入这些方法。
  1. deposit方法:
  • 当一个线程调用deposit方法时,它会获取BankAccount对象的锁,然后执行balance += amount;操作,增加账户余额。
  • 在执行完balance += amount;操作后,释放BankAccount对象的锁。
  1. withdraw方法:
  • 当一个线程调用withdraw方法时,它会获取BankAccount对象的锁,然后执行balance -= amount;操作,减少账户余额。
  • 在执行完balance -= amount;操作后,释放BankAccount对象的锁。
  1. transfer方法:
  • 当一个线程调用transfer方法时,它会依次执行this.withdraw(amount);和toAccount.deposit(amount);两个操作。
  • 由于这两个操作都是同步方法,因此在执行this.withdraw(amount);和toAccount.deposit(amount);时,同一时刻只有一个线程能够执行这些操作,从而避免了并发问题。
  1. getBalance方法:
  • 当一个线程调用getBalance方法时,它会获取BankAccount对象的锁,然后读取账户余额并返回。
  • 在读取完账户余额后,释放BankAccount对象的锁。

总结:通过synchronized修饰符,我们确保了对BankAccount对象的各个方法的访问是线程安全的,即在同一时刻只有一个线程能够执行这些方法,从而避免了并发访问导致的数据不一致性问题。

案例二:使用ReentrantLock的打印队列

假设我们有一个打印队列,多个用户可能会同时发送打印任务,我们使用ReentrantLock来同步任务的提交。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class PrintQueue {
    private final Lock queueLock = new ReentrantLock();
    public void printJob(Object document) {
        queueLock.lock(); // 获取锁
        try {
            // 模拟打印任务需要一段时间
            long duration = (long) (Math.random() * 10000);
            System.out.println(Thread.currentThread().getName() + ": PrintQueue: Printing a job during " + (duration / 1000) + " seconds");
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock(); // 确保锁被释放
        }
    }
}

代码的运行过程:

  1. 首先,我们定义了一个名为 PrintQueue 的类,其中包含一个名为 queueLock 的 ReentrantLock 对象,用于控制对打印队列的访问。
  2. printJob 方法是一个模拟打印任务的方法。在这个方法中,我们首先调用 queueLock.lock() 来获取锁,确保只有一个线程可以访问打印队列。
  3. 然后,我们模拟了打印任务需要一段时间,使用 Thread.sleep 方法来让当前线程休眠一段随机时间,模拟打印任务的耗时。
  4. 在 try 块中,我们使用 Thread.sleep 来模拟打印任务的时间,并在控制台打印出当前线程的名称以及打印任务的耗时。
  5. 在 finally 块中,我们调用 queueLock.unlock() 来确保锁被释放,无论是否发生异常,都会释放锁。

现在让我来解释一下整个代码的运行过程:

  • 当一个线程调用 printJob 方法时,它会首先尝试获取 queueLock 对象的锁。
  • 如果当前没有其他线程持有该锁,那么这个线程会成功获取锁,并且可以执行打印任务。
  • 如果另一个线程已经持有了锁,那么当前线程将被阻塞,直到锁被释放。
  • 当打印任务完成后,无论是否发生异常,finally 块中的 queueLock.unlock() 语句都会确保锁被释放,以便其他线程可以获取锁并执行打印任务。

这样,通过使用 ReentrantLock 对象,我们可以确保对打印队列的访问是线程安全的,避免了多个线程同时访问打印队列可能引发的问题。

相关文章
|
安全 Java
【Synchronized关键字】
【Synchronized关键字】
|
3月前
|
安全 Java
synchronized关键字
在Java中,`synchronized`确保多线程安全访问共享资源。应用于实例方法时,锁绑定于对象实例,仅阻止同一对象的其他同步方法访问;应用于静态方法时,锁绑定于整个类,阻止该类所有同步静态方法的同时访问。实例方法锁作用于对象级别,而静态方法锁作用于类级别,后者影响所有对象实例。正确应用可避免并发问题,提升程序稳定性和性能。
|
4月前
|
安全 Java 开发者
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
59 0
|
6月前
synchronized与ReentrantLock区别与联系
synchronized与ReentrantLock区别与联系
40 0
|
存储 安全 Java
JUC第五讲:关键字synchronized详解
JUC第五讲:关键字synchronized详解
|
程序员
ReentrantLock与synchronized的区别
ReentrantLock与synchronized的区别
|
存储 安全 Java
synchronized关键字讲解
synchronized关键字讲解
synchronized关键字讲解
|
缓存 Java 编译器
深入理解synchronized关键字
synchronized关键字详解
88 0
java多线程关键字volatile、lock、synchronized
volatile写和volatile读的内存语义: 1. 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。 2. 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。 3. 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
287 0
|
存储 缓存 安全
深入理解synchronized关键字(一)
深入理解synchronized关键字(一)
194 0
深入理解synchronized关键字(一)