(1)两者都是可重入锁
可重入锁指的是在一个线程中可以多次获取同一把锁,比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁,两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
synchronized
:计数器涉及到ObjectMonitor
类中的_count
变量(数值为0表示没有线程占用锁,数值为1表示有线程占用锁)和_recursion
变量(数值表示同一个线程对锁的重入次数)。一个线程执行带锁的方法A第一次获取锁,此时_count=1,recursion=1,接着该线程调用另一个需要相同锁的方法B再次获取锁,此时_count=1,recursion=2,方法B调用完成释放一次锁,此时_count=1,recursion=1,最后方法A调用完成,_count=0,recursion=0。(期间若有其他线程竞争锁,_count保持不变,该线程进入EntryList队列,转为blocked状态)ReentrantLock
:计数器涉及到AQS
类中的state
变量,为0表示没有线程占用锁,大于0表示有线程占用锁,该线程对锁的重入次数。
(2)synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们;
ReentrantLock 是 JDK 层面实现的(也就是个 API ,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
synchronized
是 Java 语言内置的关键字,是在 JVM 层面实现的。JVM 负责对synchronized
进行底层的锁管理,包括锁的获取、释放、锁的升级和降级等操作。ReentrantLock
是 JDK 提供的锁实现,是一个基于接口和类的 API。它在 Java 层面使用了一些底层的同步器类(例如AbstractQueuedSynchronizer
)来实现锁的管理。
(3)ReentrantLock 比 synchronized 增加了一些高级功能
主要来说主要有三点:
- 等待可中断,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情;
- 可实现公平锁,ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的;
- 可实现选择性通知(锁可以绑定多个条件),一个ReentrantLock对象中可以创建多个Condition实例,线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。而synchronized相当于一个ReentrantLock对象中只有一个Condition实例,所有线程注册在该实例身上,在使用notify()/notifyAll()方法进行通知时,由JVM随机选一个线程唤醒或者将全部线程唤醒。用ReentrantLock类结合Condition实例,调用signalAll()方法,在该Condition实例上注册的所有等待线程全部唤醒,可以实现“选择性通知”。(调用某个Condition实例的signal()方法,在该Condition的列表中唤醒firstWaiter指向的线程(最先放进去的线程))
(4)性能
JDK1.6之前,synchronized性能远逊于ReentrantLock,JDK1.6之后(包括JDK1.6)synchronized性能和ReentrantLock持平。
synchronized
在 JDK 1.6 以后经过了许多优化,性能已经得到了很大的提升。它使用了偏向锁、轻量级锁、重量级锁等技术,以提高并发性能。ReentrantLock
相对于synchronized
,在低竞争、低并发的情况下,性能可能略逊一筹。
(5)用途
在JDK1.6版本及之后,性能不再是选择synchronized或者ReentrantLock的决定因素。如果有复杂功能需求(比如公平锁,选择性通知等),则选择ReentrantLock。而在没有复杂功能需求的情况下,优先考虑synchronized。
- synchronized是在Java语言内置的关键字,足够清晰,也足够简单。 每个Java程序员都熟悉synchronized,但JUC中的Lock接口则并非如此。 因此在只需要基础的同步功能时, 更推荐synchronized。
- Lock应该确保在finally块中释放锁, 否则一旦受同步保护的代码块中抛出异常, 则有可能永远不会释放持有的锁。这一点必须由程序员自己来保证, 而使用synchronized的话则可以由Java虚拟机来确保即使出现异常, 锁也能被自动释放。
- 尽管在JDK 5时代ReentrantLock曾经在性能上领先过synchronized, 但这已经是十多年之前的胜利了。 从长远来看, Java虚拟机更容易针对synchronized来进行优化, 因为Java虚拟机可以在线程和对象的元数据中记录synchronized中锁的相关信息, 而使用JUC中的Lock的话, Java虚拟机是很难得知具体哪些锁对象是由特定线程所持有的。
参考
- synchronized 和 ReentrantLock 有什么区别? | JavaGuide(Java面试 + 学习指南)
- ChatGPT
- 《深入理解Java虚拟机》——13.2.2 线程安全的实现方法