synchronized 与 ReentrantLock 在使用上有哪些区别?请详细比较。

参考回答
synchronized 和 ReentrantLock 都是 Java 中常用的同步工具,用于控制多线程访问共享资源时的互斥性,确保线程安全。虽然它们的作用类似,但在使用上有一些关键的区别。

语法差异:

synchronized 是一个关键字,可以用于方法或代码块中,语法简单。

ReentrantLock 是一个类,需要显式地调用 lock() 和 unlock() 方法来加锁和解锁,语法较复杂。

锁的粒度:

synchronized 通过修饰方法或代码块来指定锁的范围,粒度较大。

ReentrantLock 允许更加灵活的锁定范围,可以精确控制锁的范围和加锁解锁的位置。

死锁控制:

synchronized 无法直接控制死锁,它依赖 JVM 的调度机制。如果锁的获取顺序不一致,容易发生死锁。

ReentrantLock 可以通过 tryLock() 方法尝试获取锁,避免长时间阻塞,降低死锁的风险。

锁的可中断性:

synchronized 不能响应线程中断,线程一旦获取锁就会一直等待,直到锁可用。

ReentrantLock 可以通过 lockInterruptibly() 方法来响应线程中断,允许线程在等待锁时被中断。

公平性:

synchronized 没有公平性,线程获取锁的顺序是不确定的。

ReentrantLock 可以通过构造方法指定是否使用公平锁,如果是公平锁,线程会按照请求锁的顺序来获取锁。

性能:

synchronized 由于是 JVM 的内建机制,性能相对较低,尤其是在高并发情况下,频繁的线程阻塞和唤醒可能造成较大的性能开销。

ReentrantLock 在高并发下性能较优,特别是当使用了 tryLock() 和公平锁等特性时,它可以提高锁的效率,避免阻塞。

详细讲解与拓展
1. 语法差异
synchronized 是 Java 内建的关键字,可以非常简单地用于同步代码块或方法。

对于方法,直接在方法签名上使用 synchronized:

public synchronized void method() {
// 临界区代码
}
对于代码块,使用 synchronized关键字加锁某个对象:

synchronized (lockObject) {
// 临界区代码
}
ReentrantLock 是一个类,使用时必须显式地创建对象并调用 lock() 和 unlock() 方法来进行加锁和释放锁。

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
2. 锁的粒度
synchronized:

可以锁定整个方法,或者锁定方法中的代码块。如果是实例方法,则锁住当前实例;如果是静态方法,则锁住类的 Class 对象。

锁的粒度较大,适合简单的锁定需求。

ReentrantLock:

提供了更细粒度的控制。可以明确指定哪些代码块需要加锁,通过 lock() 和 unlock() 来精确控制锁的范围。

  1. 死锁控制
    synchronized:

如果多个线程持有不同的锁,并且以不同的顺序请求锁,则容易发生死锁。synchronized 无法主动检测和解决死锁。

ReentrantLock:

可以通过 tryLock() 来尝试获取锁,避免线程长时间等待锁,从而避免死锁。

tryLock() 可以设定超时时间,线程可以在规定时间内尝试获取锁,如果超时则退出,减少死锁发生的概率。

示例:使用 tryLock() 避免死锁:

if (lock1.tryLock() && lock2.tryLock()) {
try {
// 执行任务
} finally {
lock1.unlock();
lock2.unlock();
}
} else {
// 获取锁失败,执行其他操作
}
4. 锁的可中断性
synchronized:

不支持中断。如果线程在等待锁时被阻塞,直到锁可用,线程无法响应中断。

ReentrantLock:

提供了 lockInterruptibly() 方法,允许线程在等待锁时响应中断。如果线程在等待锁时被中断,会抛出 InterruptedException,线程可以根据需要处理中断。

示例:使用 lockInterruptibly():

try {
lock.lockInterruptibly();
// 执行任务
} catch (InterruptedException e) {
// 处理中断
} finally {
lock.unlock();
}
5. 公平性
synchronized:

synchronized 没有公平性。线程获取锁的顺序是不确定的,JVM 会根据调度策略来分配锁,这可能导致某些线程长时间无法获得锁(即“饥饿”现象)。

ReentrantLock:

支持公平锁,通过构造方法可以选择使用公平锁。公平锁会确保线程按照请求锁的顺序获取锁。公平锁避免了线程饥饿的现象,但相较于非公平锁,性能稍差。

示例:使用公平锁:

ReentrantLock lock = new ReentrantLock(true); // 创建公平锁
6. 性能
synchronized:

synchronized 是 JVM 的内建机制,性能在低并发情况下良好,但在高并发场景下可能性能较差,因为每次获取和释放锁时都需要涉及线程阻塞和唤醒的过程,带来较高的上下文切换开销。

ReentrantLock:

ReentrantLock 提供了更灵活的控制和更高的性能,尤其是在高并发场景下。通过 tryLock() 等方法,ReentrantLock 可以减少线程阻塞的次数,并且在使用公平锁时,线程可以更公平地获取锁。

总结
特性 synchronized ReentrantLock
使用简便性 使用简单,语法直观 需要显式调用 lock() 和 unlock(),语法稍复杂
锁粒度 锁定整个方法或代码块 可精确控制锁的范围
死锁控制 无法控制死锁 支持 tryLock() 避免死锁
中断支持 不支持中断 支持通过 lockInterruptibly() 响应中断
公平性 无公平性 可选择公平锁,确保锁获取的顺序
性能 在高并发场景下性能较差 在高并发场景下性能优于 synchronized
synchronized 适合简单的同步需求,特别是在低并发的场景下。

ReentrantLock 更适合高并发、需要细粒度控制、且对锁的获取有严格要求的场景,提供了更多的功能,如中断响应、超时锁、锁公平性等。

发表评论

后才能评论