你在一个咖啡店里,有一台唯一的咖啡机,顾客们需要排队使用这台咖啡机。这台咖啡机就像是一个共享资源,而synchronized
关键字和ReentrantLock
都是确保顾客能有序使用咖啡机的机制。
synchronized关键字:
想象synchronized
就像是咖啡店的一个规矩:当一个顾客正在使用咖啡机时,其他顾客必须等待。这个规矩是由咖啡店自动强制执行的,顾客们不需要额外做什么,只需等待前面的人用完。当顾客开始使用咖啡机时,他们就自动获得了使用权,完成后也会自动释放,让下一个顾客使用。这个过程很简单,但顾客们不能决定等待多久,也不能尝试中途放弃等待。
ReentrantLock:
现在想象ReentrantLock
是咖啡店提供的一个可选服务,它更像是一个高级的取号系统。当顾客进入咖啡店时,他们可以选择拿一个号码牌,这样他们就知道轮到自己的顺序了。使用ReentrantLock
,顾客可以决定他们是否愿意等待(可以设置尝试获取锁的时间),或者在等待太久后选择放弃并离开咖啡店。此外,这个取号系统还允许顾客在等待时做一些其他事情(比如读书或使用手机),这就是ReentrantLock
的可中断锁定特性。ReentrantLock
还允许顾客按照一些公平的规则排队,比如“先来后到”,但这可能会稍微减慢整个流程。
区别和应用:
synchronized
是嵌入在Java语言中的,使用起来非常简单,不需要程序员做太多的管理工作。相比之下,ReentrantLock
提供了更多的灵活性,比如可中断的锁获取、定时锁等待和公平锁选项。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
,以下是代码的运行过程:
deposit
、withdraw
、transfer
或getBalance
方法时,由于这些方法都使用了synchronized
修饰符,同一时刻只有一个线程可以执行这些方法。其他线程需要等待当前线程执行完毕后才能进入这些方法。deposit
方法:
deposit
方法时,它会获取BankAccount
对象的锁,然后执行balance += amount;
操作,增加账户余额。balance += amount;
操作后,释放BankAccount
对象的锁。withdraw
方法:
withdraw
方法时,它会获取BankAccount
对象的锁,然后执行balance -= amount;
操作,减少账户余额。balance -= amount;
操作后,释放BankAccount
对象的锁。transfer
方法:
transfer
方法时,它会依次执行this.withdraw(amount);
和toAccount.deposit(amount);
两个操作。this.withdraw(amount);
和toAccount.deposit(amount);
时,同一时刻只有一个线程能够执行这些操作,从而避免了并发问题。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(); // 确保锁被释放
}
}
}
代码的运行过程:
PrintQueue
的类,其中包含一个名为 queueLock
的 ReentrantLock
对象,用于控制对打印队列的访问。
printJob
方法是一个模拟打印任务的方法。在这个方法中,我们首先调用 queueLock.lock()
来获取锁,确保只有一个线程可以访问打印队列。
Thread.sleep
方法来让当前线程休眠一段随机时间,模拟打印任务的耗时。
try
块中,我们使用 Thread.sleep
来模拟打印任务的时间,并在控制台打印出当前线程的名称以及打印任务的耗时。
finally
块中,我们调用 queueLock.unlock()
来确保锁被释放,无论是否发生异常,都会释放锁。
现在让我来解释一下整个代码的运行过程:
printJob
方法时,它会首先尝试获取 queueLock
对象的锁。finally
块中的 queueLock.unlock()
语句都会确保锁被释放,以便其他线程可以获取锁并执行打印任务。这样,通过使用 ReentrantLock
对象,我们可以确保对打印队列的访问是线程安全的,避免了多个线程同时访问打印队列可能引发的问题。