在我们开发过程中,经常要和多线程打交道,多线程其实也是面试过程中必须问的问题,其实为什么会要使用多线程,这个大家可以百度下就清楚了。我们今天讲的这个ReentrantLock其实是对于同步(安全)的问题而产生的。大家应该都知道synchronized吧,他的作用业很简单,其实就是为了防止多线程下操作数据导致一些列问题,在1.5之前,其实都是基于这个synchronized的,而在jdk5的时候就推出了这个并发包,里面提供了与synchronized类似的工具,就是lock。
synchronized与lock区别
synchronized是java的一个关键字,synchronized是内置的语言实现,特点其实就是无需自己手动释放锁,它自己会释放的。他是阻塞的,也就是某个线程占用这个锁,其他的锁只能等待他释放。线程对于锁全部为抢占方式。
lock:lock锁最大的好处就是方便,你可以用于任何你想要使用的地方,他非常灵活,它里面也分为公平锁和非公平锁,还有他不像synchronized一直阻塞,他里面提供了一系列的方法比如(trylock方法)。他的底层都是通过cas来进行修改数据的。
在讲解ReentrantLock之前,我们必须要了解一下他的构成,我们之前一直听别人说有个叫aqs的东西,这个东西也是并发包的,其实这个aqs就是AbstractQueuedSynchronizer,他是我们并发包中的一个核心,我们的ReentrantLock以及其他类都是继承他的,他里面提供了一系列的方法,他内部实现主要是维护了一个双链表,双链表的目的其实就是为了公平锁,也就是先来先到,主要是使用一个violate修饰的state来表示锁的状态,
这个双链表里面维护了四个东西,prev和next就不用说了吧,而waitstatus变量表示的是当前的线程的状态,他有以下几个状态CANCELLED、SIGNAL、CONDITION、PROPAGATE等值,改变量的作用主要是为了判断该几点的状态。然后每次线程释放锁都会进行唤醒通知,他的唤醒通知不想notify随机唤醒,他是沿着链表来唤醒的,比如根节点释放锁的时候,他会去通知他的next节点,然后next节点就会去尝试获取锁。 而我们的ReentrantLock也是基于这个实现的,虽然他里面分为公平锁和非公平锁,其实原理都是一样的,ReentrantLock是通过内部类sync来实现我们的aqs的方法,如下图所示
而我们的源码部分主要是通过获取锁、加入队列(这边指的是双链表)、判断状态、释放锁、唤醒线程。这几部分一一讲解。主要是以公平锁来讲。
首先获取锁,也就是lock方法
final void lock() {
//其实这个就是调用acquire方法,这个在我们的aqs里面是有实现的
acquire(1);
}
我们直接去aqs里面看这个方法
public final void acquire(int arg) {
//首先会尝试获取锁,如果获取失败,则将我们线程加入到双链表中
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
到这里,我们要看一下的几个方法,第一个就是tryAcquire方法
protected final boolean tryAcquire(int acquires) {
//获取当前的线程
final Thread current = Thread.currentThread();
//获取锁的状态,我们之前说过,aqs是通过维护state来表示锁的状态,这个锁是可见的,也就是使用了voilate来修饰的
int c = getState();
//如果值为0的话代表是没有人占用锁,,然后通过cas去修改state,并返回true,代表获取成功
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//我们知道我们的锁是可重入的,这个和sync是一样的,所以如果当前的线程已经获取到锁,那么他就可以进入其他需要的锁的地方,这时候在将锁的状态+1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果都不是,则直接放回false,证明已经被占用了
return false;
}
}
这时候我们就需要执行下面的代码,也就是将我们的线程加入链表中。也就是这个方法addWaiter
*/
private Node addWaiter(Node mode) {
//先new一个节点,将线程放入该节点
Node node = new Node(Thread.currentThread(), mode);
//开始的时候tail也是为null的
Node pred = tail;
if (pred != null) {
//不为null的话,就直接将我们的节点直接放在后面
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//所以直接进入这个方法
enq(node);
//返回node节点
return node;
}
我们继续看enq(node)方法
private Node enq(final Node node) {
//这边使用了一种死循环,直到成功为止
for (;;) {
Node t = tail;
//如果为null的话就新建一个节点,然后将该节点赋值给tail,那么下一次执行的时候,就有值了
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//这时候就会将我们的这个节点加入到tail的后面,然后结束循环,这边也使用 了cas来处理,因为可能有多个线程都要加入链表
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
到这里我们的加入链表也完成了,可是还有个就是我们之前说过aqs里面是有个waitstatus的状态的,这个在哪呢,这个就是在我们这个addwiater方法前有个acquireQueued这个方法,这个方法的作用就是去设置我们这个等待状态。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//也是一个死循环,其实标准叫法叫自旋
for (;;) {
//拿到该节点的前驱
final Node p = node.predecessor();
//如果该节点的前驱是head的话,那么久去尝试获取锁
if (p == head && tryAcquire(arg)) {
//获取成功的话,就将当前节点置为头节点
setHead(node);
//然后释放之前的head节点,避免无线增长链表的长度。然后返回
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果不是的话,那么就看自己是否可以休息
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我们要继续去看这个方法shouldParkAfterFailedAcquire
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//我们这边通过传进来的前置节点和当前节点来,首先获取前置节点的waitstatus,这个之前说过总共有四种状态,每种状态表示的是不同意思
int ws = pred.waitStatus;
//如果前置节点的状态为SIGNAL的话,那么直接返回true
if (ws == Node.SIGNAL)
return true;
//如果大于0,其实也就是cancel,代表前置节点已经取消排队了,所以我们要将前置节点给剔除掉。
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {//然后设置前置节点的状态为SIGNAL,最后返回false
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果返回的是true的话,那么久会继续执行这个方法parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//判断当前线程是否被中断,被中断的话,也就意味着他可以从队列中进行移除,这时候就会调用finally的方法,也就是cancelAcquire(node);
return Thread.interrupted();
}
它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作,如果我们的线程中途被打断了,那么
我们就会执行cancelAcquire(node)方法,这个方法在finally中
private void cancelAcquire(Node node) {
//如果当前节点为null,
if (node == null)
return;
//我们自己手动将当前节点的线程置为null,
node.thread = null;
//找到该节点的前置节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//将当前节点的wait状态置为cancel
node.waitStatus = Node.CANCELLED;
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
至此我们如何获取锁的源码都解释了一篇,其实大致的过程可以分为以下几步
1、线程使用lock的方法获取锁的时候,会尝试去获取锁,如果获取成功的话就直接返回并修改锁的状态。如果发现当前线程已经持有锁的时候,还可以继续获取锁(重入锁),这时候就讲锁的状态+1.
2、如果线程获取锁失败,也就是锁的状态不为0,这时候就要将我们的线程放入到双链表中,放入链表的步骤,首先是判断tail是否为空,如果当前的线程刚好是第一个的时候,这时候我们就会new一个节点,然后将我们的这个线程放到我们new出来的节点后面(这边采用自旋的方式来加入链表,因为公平就会涉及很多线程获取不到锁,那么他们就都会加入链表中)。
3、线程加入链表的操作完成之后,我们还要考虑有些获取锁的线程是尝试去获取,也就是达到一定时间之后没有获取到,他就自我打断,不参加排队,这个时候我们就要去检查状态,看哪些是可以从链表移除,哪些是可以继续休息的,这时候就要去看我们链表中节点中维护的waitstatus的变量值,首先会去判断当前的节点的前置几点是否我head,如果是的话就尝试去获取锁,并将当前节点设置为head,并释放前置节点。
4、如果不是的话,那么就会去执行判断自己是否可以wait,这时候就是去判断waitstatus的值,如果前置节点的值为signal的话就直接返回true,如果不是的话就判断状态是否大于0,大于0表示该线程已经取消了,可以将他丢弃。如果都不是就将前置节点设置为signal,记住这边也是用了自旋方式。
5、如果当前节点的前置节点已经是signal的话,那么就将当前节点放入waiting状态,并返回他的打断状态。
6、如果被打断,那么我们就继续执行清空当前线程的节点。
至此我们如何获取锁以及状态怎么控制都讲解完了,下面我们会讲一下如何去释放锁以及如何唤醒下一个节点的线程。
首先我们直接去找unlock的方法,其实这个方法调用就是aqs的底层方法
public void unlock() {
//底层是调用aqs的release
sync.release(1);
}
release方法
public final boolean release(int arg) {
//首先尝试去释放锁,这个方法aqs没有帮我们实现,需要我们自己去重写
if (tryRelease(arg)) {
//释放锁成功,这时候就要去链表拿去头部,
Node h = head;
//判断链表的头部节点是否为null,并且他的状态是够为0,因为我们之前加入链表就将没必要的都清空,留下的应该都是signal
if (h != null && h.waitStatus != 0)
//这时候就通知head节点去获取锁
unparkSuccessor(h);
return true;
}
return false;
}
我们去看一下这个方法tryRelease,这个在ReentrantLock有实现
protected final boolean tryRelease(int releases) {
//首先会讲当前状态锁-1,主要有重入锁的存在
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果锁为0,表示释放成功,这个时候就需要将自己的线程设置为null,同时将free设置为true
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//然后改变锁的状态,也就是恢复没有被占用的状态,然后返回true
setState(c);
return free;
}
我们看一下他释放锁成功后,执行的通知head节点去获取锁的状态,unparkSuccessor
private void unparkSuccessor(Node node) {
//先获取当前头结点的转态
int ws = node.waitStatus;
//如果小于0的话,那么就将值给置为0,
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//然后在去找他下一个节点
Node s = node.next;
//如果下个节点的值为null,或者下个节点的状态为1的话,那么就直接将当前节点给置为null,然后继续去找下个节点,在释放下个节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
到此为止我们释放锁的方法也讲完了,其实不是很难,只要你跟着这个逻辑走其实不是很难,其实我们发现其实这个ReentrantLock源码中好多是直接调用aqs的,其实aqs的源码大致也就是这样的,只是有些东西需要我们通过自己的实现方式来实现的时候,就需要去覆盖掉他。aqs就是父类,假如我们自己想实现类似这样的锁的时候,也要去继承这个父类aqs,理解了aqs的这些方法,基本上这个并发报的好多东西也直接明白了,我们本篇讲的是公平锁,非公平锁也类似,大家可以根据这篇博客去看看。
waitstatus的取值就是我们上面的那几个
static final int CANCELLED = 1; 值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
static final int SIGNAL = -1;值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
static final int CONDITION = -2;与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
static final int PROPAGATE = -3;与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
还有就是个0。因为初始化是没有赋值的,所以默认就是0,