Java-并发-锁-ReentrantLock
摘要
ReentrantLock
是使用最广的、最出名的AQS(AbstractQueuedSynchronizer)
系列的可重入锁。本文会分析他的lock
, unlock
等重要方法,还涉及公平/非公平概念对比,及对比synchronized。
关于Condition
的分析,请参见本文姊妹篇:Java-并发-Condition
0x01 基本概念
ReentrantLock
是使用最广的、最出名的AQS(AbstractQueuedSynchronizer)
系列的可重入锁。它相对于LockSupport
来说属于是高层API,被鼓励在用户开发中使用。
ReentrantLock基本特点如下:
- 等待可中断
获取锁时可以指定一个超时时间,如果超过这个时间还没有拿到锁就放弃等待 - 公平性
公平锁就是按线程申请锁时候FIFO的方式获取锁;而非公平锁没有这个规则,所有线程共同竞争,没有先来后到一说 - 绑定对象
一个synchronized
绑定一个Object用来wait
,notify
等操作;而ReentrantLock可以newCondition多次等到多个Condition实例,执行await
,signal
等方法。
0x02 实现原理
限于篇幅,这里可以大概说下其原理。
2.1 AQS
AQS全称AbstractQueuedSynchronizer
,他是ReentrantLock
内部类NonfairSync
和FairSync
的父类Sync
的父类,其核心组件如下:
- state,int 类型,用来存储许可数
- Node双向链表,存储等待锁的线程
该Node
就是AQS的内部类,这里可以简单看看Node定义:
static final class Node {
// 表明等待的节点处于共享锁模式,如Semaphore:addWaiter(Node.SHARED)
static final Node SHARED = new Node();
// 表明等待的节点处于排他锁模式,如ReentranLock:addWaiter(Node.EXCLUSIVE)
static final Node EXCLUSIVE = null;
// 线程已撤销状态
static final int CANCELLED = 1;
// 后继节点需要unpark
static final int SIGNAL = -1;
// 线程wait在condition上
static final int CONDITION = -2;
// 使用在共享模式头Node有可能处于这种状态, 表示锁的下一次获取可以无条件传播
static final int PROPAGATE = -3;
// 这个waitStatus就是存放以上int状态的变量,默认为0
// 用volatile修饰保证多线程时的可见性和顺序性
volatile int waitStatus;
// 指向前一个Node的指针
volatile Node prev;
// 指向后一个Node的指针
volatile Node next;
// 指向等待的线程
volatile Thread thread;
// condition_queue中使用,指向下一个conditionNode的指针
Node nextWaiter;
// 判断是否共享锁模式
final boolean isShared() {
return nextWaiter == SHARED;
}
// 返回前驱结点,当前驱结点为null时抛出NullPointerException
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// 用来初始化wait队列的构造方法;也被用来做共享锁模式
Node() {
}
// 在addWaiter方法时,将指定Thread以指定模式放置
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// Condition使用的构造方法
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
AQS的Node等待队列双向链表如下图:
2.2 非公平锁的实现
默认采用非公平的实现NonFairSync
。
2.2.1 lock()
lock()
方法流程如下图:
可以看到,lock()
方法最核心的部分就是可重入获取许可(state),以及拿不到许可时放入一个AQS实现的双向链表中,调用LockSupport.park(this)
将自己阻塞。就算阻塞过程被中断唤醒,还是需要去拿锁,直到拿到为止,注意,此时在拿到锁之后还会调用selfInterrupt()
方法对自己发起中断请求。
2.2.2 unlock()
2.3 公平锁的实现
他的实现和非公平锁有少许区别:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
// 这里不再有非公平锁的
// if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());
// 也就是说,公平锁中,必须按规矩办事,不能抢占
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 这里多了一个 !hasQueuedPredecessors(),也就是不需要考虑wait链表
// 否则就老实按流程走acquireQueued方法
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
下面看看的hasQueuedPredecessors
实现
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// h != t 代表wait链表不为空状态
// (s = h.next) == null代表wait链表已经初始化
// s.thread != Thread.currentThread()代表当前线程不是第一个在wait链表排队的线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
0x03 ReentrantLock和synchronized对比
ReentrantLock和synchronized对比如下:
可重入 | 等待可中断 | 公平性 | 绑定对象数 | 性能优化 | |
---|---|---|---|---|---|
synchronized | 支持 | 不支持 | 非公平 | 只能1个 | 较多 |
ReentrantLock | 支持 | 支持 | 非公平/公平 | 可以多个 | - |
0x04 公平锁与非公平锁
非公平锁比起公平锁来说,唯一区别就是非公平锁可以快速用compareAndSetState(0, acquires)
进行抢占,而公平锁必须老老实实FIFO形式排队;但unlock
唤醒的时候是没有区别的。
0x05 总结
- state采用
volatile
,保证有序性和可见性 - 大量使用如
unsafe.compareAndSwapInt(this, stateOffset, expect, update);
此类的CAS操作,保证原子性,同时在竞争小的时候效率胜过synchronized
- 所谓的加锁就是AQS.state++。且该锁是可重入的,每次就state加1,unlock一次减一。两个操作必须一一对应,否则其他等待锁的线程永远等待。
- 所谓的等待锁阻塞,就是放在一个链表里,然后用
LockSupport.park(this)
阻塞 - 就算用中断唤醒已经等待锁而阻塞的线程,依然必须直到获取锁才能执行。且在其后如果执行可中断操作,会发生中断!
关于Condition
的分析,请参见本文姊妹篇:Java-并发-Condition