前言
本章总结了Lock接口,队列同步器(
AbstractQueuedSynchronizer
),可重用锁ReentrantLock
第5章 Java中的锁
一、Lock接口
显式获取/释放锁,具有可操作性、可中断的获取锁,以及超时获取锁等功能。(相比synchronized
关键字隐式锁,缺失了点隐式锁的便捷性,但功能可操作性更强。synchronized虽然可以隐式、简化锁管理,但是固化了操作,缺乏可扩展性)
基本使用:
1 | Lock lock = new ReentrantLock(); // 声明可重用锁 |
主要特性:(除了普通自旋的获取锁外,其他的特性)
特性 | 描述 |
---|---|
尝试非阻塞地获取锁 | 当前线程尝试获取锁,如果这一时刻锁没被其他线程占有,则成功获取并持有锁(此方式将只获取依次,在获取时刻内立即返回,不进行自旋) |
可被中断地获取锁 | 当获取到锁的线程被中断时,抛出中断异常,并释放锁(与synchronized不同) |
超时获取锁 | 在指定时间前获取锁,否则无法获取锁,自动返回 |
Lock接口的API:
方法名 | 描述 |
---|---|
void lock() | 自旋地获取锁,直到获取后才返回 |
void lockInterruptibly() throws InterruptException |
可响应中断的获取锁,在锁获取的过程中可以中断当前线程。 |
boolean tryLock() | 尝试非阻塞的获取锁,调用后立即返回,成功获取则返回true,否则返回false |
boolean tryLock(long time, TimeUni unit) throws InterruptException |
超时获取锁,在以下情况返回 ① 当前线程在超时时间内获取锁 ② 当前线程在超时时间内被中断 ③ 超时结束,失败返回false |
void unlock() | 释放锁 |
Condition newCondition() | 获取等待通知的组件,该组件和当前的锁绑定。当前线程只有获得了锁,才能调用该组件的wait() 方法,调用后,当前线程释放锁。 |
Lock接口的实现一般都是通过聚合使用一个同步器AbstracQueuedSynchronizer
的子类实现访问控制的。
二、队列同步器AbstractQueuedSynchronizer
队列同步器(简称同步器)AbstractQueuedSynchronizer
是用来构建锁和同步组件的基本框架,使用一个volatile的int表示同步状态,使用一个FIFO队列完成获取资源的线程的排队工作。
同步器主要使用方式:继承
子类继承同步器定义为自定义同步组件的静态内部类,通过调用已提供的方法来实现其抽象方法来管理同步状态。
同步器已经提供了3个方法来访问和修改同步状态供子类使用:
getState()
:获取当前同步状态setState(int newState)
:设置当前同步状态compareAndSetState( int expect, int update )
:使用CAS设置当前状态,该方法保证状态设置的原子性
同步器可重写的方法:(包括独占式获取/释放同步状态)
方法名 | 描述 | 备注 |
---|---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态。 | 实现该方法需要使用CAS操作查询是否符合预期后再设置同步状态 |
protected boolean tryRelease(int arg) | 独占式释放同步状态。 | 等待获取同步状态的线程将有机会获取同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取同步状态 | 获取成功则返回>=0的值。否则失败 |
protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 | |
protected boolean isHeldExclusively() | 当前同步器是否线程被独占 |
同步器提供的模板方法:
大致分为3类:
- 独占式获取与释放同步状态:
acquire
/release
(另含aquireInterruptibly
响应中断操作和tryAcquireNanos
超时等待操作) - 共享式获取与释放同步状态:
acquireShared
/releaseShared
(另含aquireSharedInterruptibly
响应中断操作和tryAcquireSharedNanos
超时等待操作) - 查询同步队列中的等待情况:
Collection<Thread> getQueuedThreads()
利用同步器实现的独占锁示例:
1 | class Mutex implements Lock { |
2.2. 队列同步器的内部实现
实现机制主要包括:同步队列、独占式同步状态获取和释放、共享式同步状态获取和释放。
2.2.1. 同步队列
同步队列是一个FIFO双向队列,每个节点保存前驱和后继节点以及对应线程。
同步队列存放获取同步状态失败的线程。
- 当同步状态释放时,唤醒首节点,后面的节点的唤醒通过前驱节点出队或被中断实现。
- 插入队列使用CAS确保原子性(因为可能会有并发插入场景),而队列头节点出队不需要CAS(因为出队只有一个线程能够获取同步状态)。
- 节点在同步状态中会自旋检查同步状态,但只有前驱节点是头节点时才能尝试获取同步状态。
- 移出队列(停止自旋)的条件是:前驱节点是头节点且成功获取了同步状态。
2.2.2. 独占式获取同步状态
流程:(同一时刻只能有一个线程访问同步状态)
2.2.3. 共享式同步状态:
同一个时刻能有多个线程同时获取到同步状态(如读写文件时,可多进程同时读1个文件,此时写被阻塞;当文件在写入时,所有读和其他写被阻塞)写为独占,读为共享。
- 使用共享式同步状态的并发组件必须使用CAS确保释放资源的原子性
锁和同步器的关系
同步器是实现锁的关键。
锁是面向使用者的,定义了使用者和锁之间的接口,隐藏了实现细节。同步器面向锁的实现者,简化锁的实现方式,隐藏了同步状态管理、线程排队、等待唤醒的操作。
三、 重入锁ReentrantLock
ReentrantLock重入锁:支持重进入的锁,表示该锁支持一个线程对资源重复加锁。有公平性和非公平性选择。
重进入:已经获取到锁的线程能够再次调用lock()而不被锁阻塞。
- 其实synchronized关键字隐式支持了重进入(如synchronized修饰的递归方法)
公平性:等待时间最长的线程优先获取锁(请求先到先得FIFO,符合请求的绝对时间顺序)
非公平性:就是不公平性
- 公平虽然能减少“饥饿”,但效率其实没有非公平高,因为会造成大量的线程切换开销代价。
- 非公平锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了更大的吞吐量
实现重进入
线程再次获取锁:识别当前线程是否为当前占据锁的线程,是则成功获取。
锁的最终释放:重复n次获取锁后,再释放n次时,其他线程能获取到该锁。
非公平锁和公平锁的实现细节
非公平锁的实现细节:
获取锁需要CAS原子操作(非公平情况下会产生并发问题)
释放锁时不存在并发,不需要CAS
没有队列上的严格限制
公平锁的实现细节:
多一个判断当前线程是否是头节点,是头节点才能获得锁(严格的FIFO)
可以看到:公平锁每次都会从队列中的第一个节点获取到锁,大量的线程切换;而非公平锁出现了一个线程连续获取锁的情况。
这是因为当一个线程请求锁时,只要获取了同步状态即成功获取锁,在此前提下,刚释放锁的线程再次获取同步状态的几率很大。
三、读写锁ReentrantReadWriteLock
读写锁在同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。
读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
(读写锁能够简化线程交互场景的编程方式,如生产者-消费者)
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。
Java提供的ReetrankReadWriteLock有三个特性(其实大概比重入锁多了锁降级):
特性 | 说明 |
---|---|
公平性选择 | 支持非公平和公平的锁获取方式 |
重进入 | 读锁或写锁都支持重进入 |
锁降级 | 获取写锁—>获取读锁—>释放写锁, 以此完成写锁降级为读锁 |
读写锁的接口示例
ReadWriteLock接口仅定义了读锁和写锁两个方法:
方法名 | 描述 |
---|---|
readLock() | 获取读锁 |
writeLock() | 获取写锁 |
其实现ReetrantReadWriteLock还提供了外界监控锁状态的方法:
方法名 | 描述 |
---|---|
int getReadLockCount() | 返回当前读锁被获取的次数(不等于获取读锁的线程数) |
int getReadHoldCount() | 返回当前线程获取读锁的次数(参数保存在ThreadLocal中) |
boolean isWriteLocked() | 判断写锁是否被获取 |
int getWriteHoldCount() | 返回当前写锁被获取的次数 |
使用示例:
1 | public class Cache { |
读写状态设计
读写锁使用一个int变量(32位),维护多个读线程和写线程的状态
写状态:被一个线程重复获取写锁的次数;
读状态:是所有线程获取读锁次数的总和
按位切割方法:高16位表示读,低16为表示写(按2进制16位数计算锁获取次数)
推论:同步状态S不为0时,写状态为0时时,则读状态(S >>> 16) 定大于 0,即读锁被获取
1 | 读锁被获取判断:((S != 0) && (S & 0x0000FFFF == 0)) => ((S >>> 16) > 0) |
锁降级
获取写锁—>获取读锁—>释放写锁
好处:锁降级是为了保证数据的可见性,如果不是锁降级而是释放写锁再获取读锁,中途可能会被别的线程写打断下一步读。锁降级同时也可以保证其他读线程能获取读锁,达到并发读的目的。
LockSupport工具
LockSupport
也是构建同步组件的基础工具之一,提供了一组另线程阻塞和唤醒功能的公共静态方法
park(Object blocker)
用于在某对象上阻塞当前线程(此外还有超时等待方法xxNanos和xxxUntil)
unpark(Thread th)
方法唤醒一个被阻塞的线程
Condition接口
Condition接口实现的功能与Object监视器的等待/通知机制类似(await/signal - wait/notify
)
Condition是同步器AbstractQueuedSynchronizer的内部类
调用Condition需要先获取其关联的Lock锁(一个Condition必须由Lock的newCondition()方法创建出来) 使用前需获取lock锁
Condition部分方法:
方法名 | 描述 |
---|---|
void await() throws InterruptedException | 可被中断地等待,直到被signal通知 |
void awaitUninterruptibly() | 不响应中断的等待,直到被signal通知 |
long awaitNanos(long nanosTimeOut) throws InterruptedException | 超时等待 |
boolean awaitUntil(Date deadline) throws InterruptedException | 等待到某个时间,中途被通知返回true,否则返回false。响应中断 |
void signal() | 唤醒一个等待在Condition上的线程,该线程返回前必须获得Condition相关联的锁 |
void signalAll() | 唤醒所有等待在Condition上的线程 |
示例:
1 | Lock lock = new ReetrantLock(); // 创建锁实例 |
对比Object监视器和Condition
对比项 | Object Monitor Methods | Condition |
---|---|---|
前置条件 | 获取对象锁(synchronized ) |
调用Lock.lock() 获取锁调用 Lock.newCondition() 获取Condition对象 |
调用方式 | 直接在object对象上调用 如 object.wait() |
直接使用condition调用 如: condition.await() |
等待队列个数 | 一个 | 多个(一个Condition本身维护一个等待队列,一个同步器上有多个Condition和一个同步队列) |
当前线程释放锁并进入等待 | 支持(wait) | 支持(await) |
线程释放锁后,在等待中不响应中断 | 不支持 | 支持 |
线程释放锁进入超时等待 | 支持 | 不支持 |
线程释放锁等待到将来一时间 | 不支持 | 支持 |
唤醒等待队列中一个或全部线程 | 支持 (notify) | 支持 (signal) |
Condition等待队列和同步器的联系
Condition是同步器AbstractQueuedSynchronizer的内部类,同步器维护的多个等待队列和一个同步队列,而每个等待队列都是由每个Condition维护的,也是存放着获得锁失败的线程。
signalAll相当于该Condition上的每一个节点均执行以此signal()方法,所有节点移到同步队列中,并唤醒它们。