您的位置:首页 > 其它

ReenTrantLock技术内幕(一)

2016-03-19 23:54 274 查看
AbstractQueuedSynchronizer(AQS)是JUC的基石,是由伟大的Doug Lea设计完成。具体的设计过程可以看他写的论文,介绍的非常详细。AQS其实是一个抽象类,但内部已经帮我们实现了许多的核心方法。通过利用CAS操作与改进CLH队列来完成同步操作并且提供了一种细颗粒的锁机制,我们知道ReenTrantLock的底层就是通过实现AQS来完成同步操作。

在AQS中提供了下面五个方法的接口,供子类实现:

protected boolean tryAcquire(int arg)

protected boolean tryRelease(int arg)

protected int tryAcquireShared(int arg)

protected boolean tryReleaseShared(int arg)

protected boolean isHeldExclusively()


这里在实现过程中一般有两种模式,一种是共享模式、一种是独占模式。共享模式表示多个线程能够共享摸个锁,独占模式表示只有一个线程能够获取方法执行权,其他线程必须等待。下面直接看一下ReenTrantLock的实现原理。

ReenTrantLock默认是非公平锁,但是在使用ReenTrantLock的过程中,所有的操作都是委派至一个Sync类上,Sync类继承了AQS,如下图所示:



而NonfairSync表示已非公平锁形式实现AQS,FairSycn表示使用公平所形式实现AQS。

CLH队列

在AQS中,通过改进CLH队列结构来管理线程状态,以便完成tryLock、lockInterruptibly等操作。先看一下AQS中CLH队列的元素:



可以看到AQS将CLH队列设计成为一个阻塞队列,每一个节点表示需要竞争资源的线程,而每个节点有自身的状态,它只需要关注自身的状态与前驱节点状态就行,而且通过队列管理,对资源竞争的操作只出现在队列头与队列尾,这大大降低了系统的压力,提升了系统的性能。下面我们直接看源码。

之前介绍的ReentrantLock中有两种锁,一种是公平锁,一种是非公平锁。下面看一下公平锁的lock方法:


final void lock() {

acquire(1);

}


可以看到FairSync的lock方法直接调用了AQS的acquire方式来获取锁。我们看一下AQS中的acquire方法代码:

public final void acquire(int arg) {

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}


这里有3个方法需要我们注意,其实tryAcquire是AQS中的抽象方法,留给子类实现也就是ReenTrantLock来实现。若获取锁成功,则tryAcquire返回true,在这里若获取锁失败,则进入acquireQueued方法。我们先看一下addWaiter方法,看他会传递给acquireQueued方法什么东西。

private Node addWaiter(Node mode) {

//创建一个新的节点,mode为Node.EXCLUSIVE,表示的是获取锁的方式为独占方式

Node node = new Node(Thread.currentThread(), mode);

// 获取当前CLH队列中的队尾节点

Node pred = tail;

//若队尾节点部位空,则将需要插入的node的前置节点设为队尾节点pred

if (pred != null) {

node.prev = pred;

//将新节点node通过CAS操作插入到队尾,若修改失败则表示有其他节点竞争队尾位置,进入enq方法

if (compareAndSetTail(pred, node)) {

pred.next = node;

return node;

}

}

//进入enq方法

enq(node);

return node;

}

private Node enq(final Node node) {

//这是一个自旋操作

for (;;) {

//获取当前队尾

Node t = tail;

if (t == null) { // Must initialize

//若为空,则表示当前队列是一个空队列并没有线程持有锁,则新建一个node节点并使用CAS操作设置队列头节点。

if (compareAndSetHead(new Node()))

tail = head;

} else {

//若不为空,则继续自旋CAS操作,尝试将node设置为队尾节点

node.prev = t;

if (compareAndSetTail(t, node)) {

t.next = node;

return t;

}

}

}

}


由上述方法可以看出,在AQS的CLH队列中,队列头表示拥有执行权的线程,后续线程需要获取锁,则需要加入到CLH队列尾部,若CLH队列为空,则表示该线程能直接获取锁。addWaiter返回一个node,代表着当前CLH的队尾。现在看一下acquireQueue方法的代码

final boolean acquireQueued(final Node node, int arg) {

boolean failed = true;

try {

boolean interrupted = false;

for (;;) {

//获取当前节点的前一个节点,也就是我们刚才addWaiter返回的node节点的前置节点。

final Node p = node.predecessor();

//若该前置节点是头结点,且当前节点已经获取到锁,那么将node设置为头结点并返回。

if (p == head && tryAcquire(arg)) {

setHead(node);

p.next = null; // help GC

failed = false;

return interrupted;

}

//若不是,则判断前一个节点的状态,看是需要挂起还是中断

if (shouldParkAfterFailedAcquire(p, node) &&

//若需要挂起,则使用LockSupport中的park方法把当前线程挂起。

parkAndCheckInterrupt())

interrupted = true;

}

} finally {

if (failed)

//若果有异常,则取消请求,将当前节点从队列中移除。

cancelAcquire(node);

}

}


现在我们在理一下lock方法的执行顺序:先执行acquire方法,acquire方执行时,会先调用自行实现的tryAcquire获取锁,若获取锁成功,则直接执行线程;若不成功,则为当前线程创建一个node,加到CLH队列尾部,可以看到需要竞争锁线程进入到CLH队列中后,就要开始控制访问了,通过acquireQueued方法进行自旋,直到线程获取锁或者中断退出,在获取锁的期间,线程会处于阻塞状态,直到被唤醒获取锁。在AQS中只有一个线程能够在同一时刻继续运行,其他的进入等待状态。每一个线程是一个独立的个体,他们自己观察,当条件满足的时候(前节点是队列头且其CAS获取锁1操作成功),那么该线程就能运行了。下面来看一下ReenTrantLock中对tryAcquire的两种实现方式

1.公平获取锁

protected final boolean
ad7f
tryAcquire(int acquires) {

//先获取当前线程并通过getState获取当前线程的状态

final Thread current = Thread.currentThread();

int c = getState();

//若为0则执行以下操作:

//1.通过hasQueuedPredecessors判断当前线程是否为CLH的头结点

//2.通过CAS操作设置当前节点的状态

//3.设置成功则让当先线程获取执行权,返回true

if (c == 0) {

if (!hasQueuedPredecessors() &&

compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

}

//由于ReenTrantLock是可重入锁,所以这里获取当前拥有锁的线程,因为lock,unlock方法必须成对出现

else if (current == getExclusiveOwnerThread()) {

//进行计数

int nextc = c + acquires;

if (nextc < 0)

throw new Error("Maximum lock count exceeded");

setState(nextc);

return true;

}

return false;

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  技术