ReentrantLock简介
首先回顾一下synchronized关键字。
把代码声明为synchronized之后,那么就会保证,每次都只有一个线程获取对象的内部锁,进而产生互斥保证共享资源的安全。synchronized是获取对象的内部锁,所以是原生语法层面的互斥,需要JVM实现。
ReentrantLock是jdk1.5开始引入的JUC并发包中的一个类,ReentrantLock基于java代码实现,也就是API层面的互斥。ReentrantLock是锁的实现类,这就让它更具有灵活性,可以用多种算法来实现。而且在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上)
要理解ReentranLock.我们先从它的整体结构和阅读源码开始学习。
在学习前可以先看看ReentrantLock的基本工作流程。然后带着流程去理解源码。这个流程图是在学习之后总结出来的。这里在前面也放一份。
ReentrantLock类中的方法列表:
// 创建一个 ReentrantLock ,默认是“非公平锁”。
ReentrantLock()
// 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁。
ReentrantLock(boolean fair)
// 查询当前线程保持此锁的次数。
int getHoldCount()
// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
protected Thread getOwner()
// 返回一个 collection,它包含可能正等待获取此锁的线程。
protected Collection<Thread> getQueuedThreads()
// 返回正等待获取此锁的线程估计数。
int getQueueLength()
// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition)
// 返回等待与此锁相关的给定条件的线程估计数。
int getWaitQueueLength(Condition condition)
// 查询给定线程是否正在等待获取此锁。
boolean hasQueuedThread(Thread thread)
// 查询是否有些线程正在等待获取此锁。
boolean hasQueuedThreads()
// 查询是否有些线程正在等待与此锁有关的给定条件。
boolean hasWaiters(Condition condition)
// 如果是“公平锁”返回true,否则返回false。
boolean isFair()
// 查询当前线程是否保持此锁。
boolean isHeldByCurrentThread()
// 查询此锁是否由任意线程保持。
boolean isLocked()
// 获取锁。
void lock()
// 如果当前线程未被中断,则获取锁。
void lockInterruptibly()
// 返回用来与此 Lock 实例一起使用的 Condition 实例。
Condition newCondition()
// 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
boolean tryLock()
// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
boolean tryLock(long timeout, TimeUnit unit)
// 试图释放此锁。
void unlock()
在来看一下它的源码,如果贴上全部源码可能会看的有点头晕。所以可以自行在Eclipse中打开源码。这里做一个简单的归类,以及后面一点一点的读。如图:
类图,ReentrantLock是Lock类的实现类,它有一个自己的内部类Sync.ReentrantLock将Lock类的大部分实现,全部委托给了Sync来实现。现,AbstractQueuedSynchronizer中抽象了绝大多数Lock的功能,而只把tryAcquire方法延迟到子类中实现。Sync同时有两个子类,一个就是用于公平锁,一个用于非公平锁。下面就学习一下,是如何实现的。
锁的实现(加锁)
查看ReentrantLock API可以看到有一个方法lock()获取锁。源码如下:
public void lock() {
sync.lock();
}
前面说了,大部分实现都是委托给了Sync这个类。而Sync有两个子类,先学习公平锁,理解了公平锁,对于非公平锁也容易多了。因此查看Sync子类 FairSync中的 lock()方法:
final void lock() {
acquire(1);
}
发现调用了一个acquire(1)方法,这个方法是干嘛的呢?从类图可以看到Sync继承了AbstractQueuedSynchronizer。而acquire(1)就是其AQS的一个方法。点击查看源码:
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
看其注释,这个方法是独占模式下的一个方法,并且忽略中断。如果获取到资源,就直接返回,否则就进入一个队列。直到获取到资源为止。我们获取资源第一个真方法可以说是这个才正式开始,那么这个方法也可以理解为独占模式下获取资源的顶层入口。然后分析方法:
方法中有一个判断,如果为真,就执行一个方法 selfInterrupt()。从方法名称来看,这个是自我中断。那么综合方法的注释——在获取资源时候,忽略中断。知道获取资源。那么方法就可以拆分2部分
1.获取资源,然后判断。
2.根据判断的真假决定是否执行自我中断(selfInterrupt())
先看第一步,if中包含两个方法。
1.tryAcquire(arg)
2.acquireQueued(addWaiter(Node.EXCLUSIVE), arg));
tryAcquire(arg)
tryAcquire是Sync的一个方法。源码:
/**
* 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) {
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;
}
}
该方法表示试图获取锁,如果获取成功直接返回true否则返回false。这里又分为2步骤
1.判断c是否等于0. 如果c等于0,那么就判断中执行hasQueuedPredecessors() 与compareAndSetState(0, acquires)。如果条件为真,就执行 setExclusiveOwnerThread(current) 并返回ture。
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;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
这个方法就是判断当队列中是否有其它线程,如果没有,说明没有线程占有着锁。关于这个线程队列,下次会做个详细学习笔记。所以当hasQueuedPredecessors 返回false,说明当前队列没有线程,那么就执行compareAndSetState(0, acquires)方法。这个方法其实就是前面学习过类似的方法,CAS更新。把state更新为1.然后把独占线程设置为当前线程,就是说让当前线程获取到资源
那么为什么非得是c==0进来呢。那就看一下c不等于0是怎么执行的。
不等与0的时候执行current == getExclusiveOwnerThread()判断当前线程是否就是当前占锁的线程。那么这就可以理解,如果c==0就是说这个时候锁是空着的,没有任何线程占有,不等于0就是说明锁被占着。所以在上面,当更新完状态之后,我们就把锁给当前线程。那锁被占了为什么还要判断锁是不是当前线程占有,这是因为ReentrantLock是可重入锁,所以就要判断一次。如果是,就把c加上acquires然后更新状态值。返回。
这样我们就梳理一下:
在lock中调用的acquire(1),这个常量1就代表锁获取一次就需要更新的状态值,如果是第一次获取资源,则变为1,如果是重入那么就在原来的基础上加1.因此:ReentrantLock的锁的一个机制,是每当线程获取一次相同锁,就进行一次计数加1.同理,如果释放一次就减1.如果计数为0说明就是释放了锁
总结tryAcquire方法就是尝试的去获取一下资源,如果获取就返回true否则返回false.
因此在在尝试获取失败后再执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。
addWaiter 方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) { //队列不为空就加入
node.prev = pred;
if (compareAndSetTail(pred, node)) { //进行CAS操作更新节点。如果失败代表有并发,进入enq方 法
pred.next = node;
return node;
}
}
enq(node);
return node;
}
嗯!在分析这个源码之前,先来看一下Node是什么。
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
首先他是AbstractQueuedSynchronizer的一个静态的内部类。这个跟LinkedList源码中的Node类似。我们刚才说的队列就是由这个Node组成。组成的队列就是AQS中的CLH队列。关于CLH队列后续会写一遍学习笔记来仔细记录。这里我们需要理解的就是这个Node会把在一个个获取资源的线程串联成一个FIFO(先进先出)的队列。这样就保证了公平性。因此addWaiter 就是如果这个队列不为空就直接把线程加入队列,加入失败就代表有并发竞争,那就进入enq死循环。直到添加成功为止
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
OK。这个时候线程已经被封装成一个节点,并且加入了队列中。接下来要做一件什么事情呢?线程加入队列不可能还要一直运行着。所以需要将它挂起来。所以这个任务就交给acquireQueued来实现。
acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//获取节点的上一个节点
if (p == head && tryAcquire(arg)) {//如果上一个节点是头节点,并且常识获取锁成功。就把当前节点设置为头节点。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted; //此处返回false.就是避免前面selfinterrupt方法执行。
}
if (shouldParkAfterFailedAcquire(p, node) && //如果不当前节点不是头节点或者头节点获取资源失败。那么就意味着要等待头节点的下一次获取,那么判断当前线程是否需要挂起。
parkAndCheckInterrupt()) //如果当前线程需要挂起就调用LockSupport类中的park方法将线程挂起。然后进入下一次循环。
interrupted = true;
}
} finally {
if (failed) //抛出异常就把节点从队列中移除
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
判断当前是否需要挂起是通过 waitStatus值来判断。
Node节点中,除了存储当前线程,节点类型,队列中前后元素的变量,还有一个叫waitStatus的变量,改变量用于描述节点的状态,为什么需要这个状态呢?
原因是:AQS的队列中,在有并发时,肯定会存取一定数量的节点,每个节点 代表了一个线程的状态,有的线程可能可能“等不及”获取锁了,需要放弃竞争,退出队列,有点线程在等待一些条件满足,满足后才恢复执行(这里的描述很像某个J.U.C包下的工具类,ReentrankLock的Condition,事实上,Condition同样也是AQS的子类)等等,总之,各个线程有各个线程的状态,但总需要一个变量买描述它,这个变量就叫waitStatus,它有四种状态:
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
**重点内容** static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
CANCELLED:因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收;
SIGNAL:表示这个结点的继任结点被阻塞了,到时需要通知它;
CONDITION:表示这个结点在条件队列中,因为等待某个条件而被阻塞;
PROPAGATE:使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件传播;
当且仅当前一个节点处于SIGNAL时候,才需要挂起。
释放锁
获取锁之后,是必须要在最后释放锁的。理解了获取锁,释放锁就容易多了。
释放操作需要做哪些事情:
1. 因为获取锁的线程的节点,此时在AQS的头节点位置,所以,可能需要将头节点移除。
2. 而应该是直接释放锁,然后找到AQS的头节点,通知它可以来竞争锁了。
到此ReentrantLock的公平锁基本分析完毕。那么还有一个非公平锁。非公平锁其实就是抢占式的。先看源码:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
源码中就是非公平锁的实现,就是在执行公平锁之前,先不管三七二十一,先去获取一次锁,如果获取成功,直接返回,否则就老老实实的排队进入公平锁。让我想到买火车票排队。人太多,有些人就懒得排队,就会跑到最前面问售票员,可不可以帮我先买一张,我很急。售票员如果给一个白眼,那么他就公平了,如果售票员同情心起来,卖给他,那就非公平性了。
总结
源码的学习就在于能知其然。所以可能学习的过程会比较困难。但是希望自己坚持这种学习方式。到这里ReentrantLock基本学习分析完毕。其中涉及到的CLH队列。会做一个新的篇章学习。最后画一个流程图。来表示ReentrantLock的基本流程: