ReentrantLock
和 synchronized
都是 Java 中的锁机制,主要用于实现线程间的互斥访问,确保线程安全。它们有一些共同点,也有各自的特性和区别。以下是二者的详细对比:
一、共同特点
-
可重入性:两者都是可重入锁,即同一线程可以多次获取该锁而不会被阻塞。可重入性允许递归调用带锁的方法而不会导致死锁。
-
互斥性:两者都提供了互斥访问机制,确保同一时间只有一个线程能够进入受保护的代码块。
-
线程安全性:两者都可以保证线程安全,确保共享资源在多线程环境下不出现数据不一致的情况。
-
内存可见性:两者在解锁之前,都会将工作内存中的修改刷新到主内存中,这样其他线程可以看到最新的共享变量值,保证了变量在多线程环境下的可见性。
二、区别
-
获取锁的方式:
-
ReentrantLock
:可以使用lock()
方法手动获取锁,同时提供tryLock()
和lockInterruptibly()
方法,可以更灵活地控制锁的获取,例如实现带超时的锁获取,响应中断等。 -
synchronized
:自动获取锁,不支持手动控制和带超时的获取,一旦进入等待状态,只能等到锁被释放或线程被中断。
-
-
锁的释放:
-
ReentrantLock
:需要显式调用unlock()
方法来释放锁,通常放在try-finally
结构中,以确保锁的正确释放。 -
synchronized
:锁的释放由 JVM 自动管理,当持有锁的线程退出同步代码块时,锁会自动释放。
-
-
公平锁和非公平锁:
-
ReentrantLock
:可以选择公平锁(严格按照请求顺序分配)或非公平锁(可能让新来的线程插队)。通过构造函数new ReentrantLock(true)
来创建公平锁。 -
synchronized
:始终是非公平锁,不保证线程获得锁的顺序。
-
-
性能:
-
ReentrantLock
:在高并发环境中表现良好,非公平锁模式下性能优于synchronized
。它允许更多的控制和优化。 -
synchronized
:从 Java 6 起,synchronized
得到了大量优化,如偏向锁、轻量级锁和自适应自旋锁的引入,使得synchronized
在低竞争环境下开销更小。
-
-
中断响应:
-
ReentrantLock
:支持线程在等待锁时响应中断(lockInterruptibly()
),适用于需要灵活控制的场景。 -
synchronized
:不支持中断等待,线程在等待锁时不能响应中断。
-
-
Condition条件等待:
-
ReentrantLock
:可以结合Condition
实现多条件等待和唤醒,类似于Object.wait()
和Object.notify()
,但更加灵活。可以在一个锁上创建多个Condition
实例,实现不同的等待队列。 -
synchronized
:依赖于wait()
、notify()
和notifyAll()
实现线程间通信,且同一对象只能有一个等待队列,控制较为简单。
-
-
灵活性:
-
ReentrantLock
:提供更丰富的 API 和控制手段,例如支持超时获取锁、可以查询锁状态、实现多个条件等待等,适用于复杂的并发控制场景。 -
synchronized
:语法简单,适合基本的互斥需求,但灵活性较低。
-
三、使用建议
-
ReentrantLock
适合需要复杂锁控制的场景,如实现公平锁、响应中断、带超时的锁、多个条件等待等高并发业务逻辑。 -
synchronized
适合简单的同步操作,并且由于其简单直接和 JVM 优化,在普通的线程互斥场景中推荐使用。
总结
特性 | ReentrantLock |
synchronized |
---|---|---|
可重入性 | 支持 | 支持 |
互斥性 | 支持 | 支持 |
锁的释放 | 需要手动释放 (unlock() ) |
自动释放 |
公平锁 | 支持 | 不支持 |
性能 | 高并发环境性能较优 | 优化后在低并发环境性能较优 |
中断响应 | 支持 | 不支持 |
条件等待 |
Condition 可实现多个条件 |
wait() 、notify() 、notifyAll()
|
灵活性 | 高,适合复杂控制 | 低,适合基本互斥需求 |
选择哪种锁应基于具体业务需求。简单的同步建议使用 synchronized
,而复杂的并发控制场景可以考虑 ReentrantLock
。