前言
相关系列
- 《Java & Lock & 目录》
- 《Java & Lock & 源码》
- 《Java & Lock & 总结》
- 《Java & Lock & 问题》
涉及内容
- 《Java & Lock & Condition & 总结》
- 《Java & Lock & ReentrantLock & 总结》
- 《Java & synchronized & 总结》
概述
简介
Lock @ 锁接口定义了API层面上锁的概念。谈到锁,我们很容易能联想到关键字synchronized,其是Java在JVM层面上提供的用于在多线程环境下对共享资源进行同步访问的锁实现。因此自然而然地,我们会将锁接口与之进行对比。但实际上如果从功能的角度来说,将两者进行对比是不合适的。因为synchronized关键字是真正能够提供锁功能的实现,而锁接口仅仅是在API层面上对锁概念进行了定义,并无法提供实际的锁功能。因此两者在维度上是不平等的,如果真的要与synchronized关键字在具体功能上进行对比的话,使用锁接口的经典实现类ReentrantLock @ 可重入锁会更合适,因为两者都是基于重入/独占特性的实现。但话虽如此,如果从结构的角度来看,将两者进行对比也并非完全不可行。例如相比synchronized关键字只加不解(其解锁由JVM自动实现,在手动的角度上只有加锁而没有解锁的说法)的结构而言,锁接口将加/解锁都定义为了人为操作。该结构定义使得锁接口实现类的使用变得稍显复杂,因为开发者需要人为保证“加锁后一定解锁,解锁前必须加锁”的行为准则不被破坏,因此锁接口实现类通常都需要搭配try-catch-finally使用。但相对地这也令锁接口实现类在使用上具备了更强的灵活性,使得多个锁域间的交互除了包含外还可以出现如下图般的交叉情况存在。
锁接口定义了阻塞/阻塞可中断/特殊值/超时四种加锁形式。与synchronized关键字只能无限等待到成功加锁不同,锁接口为加锁定义了四种不同的形式以使得实现类能够满足开发中的实际场景所需,其具体名称及表现形式如下所示:
阻塞:如果锁允许被持有则加锁并返回;否则无限等待至锁允许被持有为止。线程在进入方法/等待持有期间如果已/被中断不会抛出中断异常,但中断状态会被保留。
阻塞可中断:如果锁允许被持有则加锁并返回;否则无限等待至锁允许被持有为止。线程在进入方法/等待持有期间如果已/被中断会抛出中断异常,但中断状态会被清除。
特殊值:如果锁允许被持有则加锁并返回true;否则返回false。
超时:如果锁允许被持有则加锁并返回true;否则有限等待至锁允许被持有为止;超出指定等待时间则返回false。线程在进入方法/等待持有期间如果已/被中断会抛出中断异常,但中断状态会被清除。
锁接口允许锁关联多个条件。所谓条件,本质是Condition @ 条件接口的实现类对象,该接口被设计用于定义条件机制。条件机制的本质是线程管理机制,用于在并发环境下实现对线程的有序协调及精确控制,即令线程有序的等待/唤醒。锁接口引入条件机制目的是为了对访问锁的线程提供更高层面的管理,之所以说是更高等级,是因为锁作为具备令线程等待/唤醒定义的API本身也属于管理机制。一个对条件机制非常大的误解是:由于条件接口在Java JDK中有且仅有AQS类内部定义的ConditionObject @ 条件对象一个实现类,并且锁接口被人熟知的经典实现类可重入锁直接复用了AQS类的条件机制实现,因此导致许多开发者产生了所谓条件机制就是AQS类/可重入锁类条件机制实现的认知,并进而将之与功能相似的synchronized关键字wait/notify @ 等待/通知机制进行对比,因为其与AQS类/可重入锁类的条件机制实现在功能上相近。这是非常错误的观点,因为条件接口/条件机制仅提供令线程等待/唤醒的基本功能定义,并不含任何逻辑性/目的性。线程具体会出于什么原因/条件/目的,又是以什么方式等待/唤醒这些都由实现类具体实现,因此AQS类/可重入锁类的条件机制只是条件机制的一种实现而无法全权代表条件机制。故而只要愿意/需要,开发者完全可以基于自身设计/需求构造一套与AQS类/可重入锁类完全无关的条件机制实现。
通过添加各种维度的特性,开发者可以实现远比synchronized关键字功能更加丰富的锁接口实现类。锁接口实现类可添加的维度包含但不限于重入/公平/独占等。锁接口实现类即可以只添加单项维度的特性,也可以多维度实现,例如可重入锁类就是典型同时兼具重入/公平/独占三项维度特性的锁接口实现类。正是因为锁接口实现类的功能完全取决自身添加的维度特性,因此将锁接口在功能上与synchronized关键字进行对比是不合理的。
与synchronized关键字在“结构”上的对比
- 锁接口定义加锁/解锁都必须人为进行,而synchronized关键字虽然需要手动加锁,但解锁却由JVM自动执行;
- 锁接口定义了阻塞/阻塞可中断/特殊值/超时四种加锁形式,而synchronized关键字只有阻塞一种;
- 锁接口定义锁允许显式关联多个条件,而synchronized关键字只能隐式关联锁对象的条件。
方法
-
void lock() —— 加锁 —— 令当前线程持有当前锁。该方法是加锁方法“阻塞”形式的实现,如果当前锁允许被持有则加锁;否则无限等待至当前锁允许被持有为止。当前线程在进入方法/等待持有期间如果已/被中断不会抛出中断异常,但中断状态会被保留。
-
void lockInterruptibly() throws InterruptedException —— 加锁(可中断) —— 令当前线程持有当前锁。该方法是加锁方法“阻塞可中断”形式的实现,如果当前锁允许被持有则加锁;否则无限等待至当前锁允许被持有为止。当前线程在进入方法/等待持有期间如果已/被中断会抛出中断异常,但中断状态会被清除。
Lock lock = new Lock();
lock.lock();
// lock.lockInterruptibly();
try {
....
} finally {
lock.unlock();
}
-
boolean tryLock() —— 尝试加锁 —— 令当前线程持有当前锁。该方法是加锁方法“特殊值”形式的实现,如果当前锁允许被持有则加锁并返回true;否则返回false。
-
boolean tryLock(long time, TimeUnit unit) throws InterruptedException —— 尝试加锁 —— 令当前线程持有当前锁。该方法是加锁方法“特殊值”形式的实现,如果当前锁允许被持有则加锁并返回true;否则有限等待至当前锁允许被持有为止,超出指定等待时间则返回false。当前线程在进入方法/等待持有期间如果已/被中断会抛出中断异常,但中断状态会被清除。
Lock lock = new Lock();
if (lock.tryLock()) {
// if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
....
} finally {
lock.unlock();
}
} else {
...
}
-
void unlock() —— 解锁 —— 令当前线程解除对当前锁的持有。如果当前线程未持有当前锁则方法会抛出不受检查异常。
-
Condition newCondition() —— 新条件 —— 基于当前锁创建条件以管理访问线程。