前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >synchronized关键字与ReentrantLock的区别和应用

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

作者头像
GeekLiHua
发布2025-01-21 13:18:23
发布2025-01-21 13:18:23
7500
代码可运行
举报
文章被收录于专栏:JavaJava
运行总次数:0
代码可运行

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关键字来同步访问共享资源,即账户余额。

代码语言:javascript
代码运行次数:0
运行
复制
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修饰符来确保depositwithdrawtransfergetBalance方法在执行时,同一时刻只有一个线程能够访问该对象的这些同步方法。

当一个线程调用BankAccount对象的方法时,如depositwithdrawtransfergetBalance,以下是代码的运行过程:

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
复制
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 的类,其中包含一个名为 queueLockReentrantLock 对象,用于控制对打印队列的访问。
  2. printJob 方法是一个模拟打印任务的方法。在这个方法中,我们首先调用 queueLock.lock() 来获取锁,确保只有一个线程可以访问打印队列。
  3. 然后,我们模拟了打印任务需要一段时间,使用 Thread.sleep 方法来让当前线程休眠一段随机时间,模拟打印任务的耗时。
  4. try 块中,我们使用 Thread.sleep 来模拟打印任务的时间,并在控制台打印出当前线程的名称以及打印任务的耗时。
  5. finally 块中,我们调用 queueLock.unlock() 来确保锁被释放,无论是否发生异常,都会释放锁。

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

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-01-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • synchronized关键字与ReentrantLock的区别和应用
    • 简介
    • 代码演示
      • 案例一:使用synchronized关键字的银行账户转账
      • 案例二:使用ReentrantLock的打印队列
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档