多线程之lock锁
lock锁中有一段代码:
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;
}
}
这段代码表示了多线程在竞争锁的逻辑。
其中,对于
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
这段代码来说,为什么没有加上同步机制?那是因为AQS提供了获取得到当前锁的线程,以及同步器状态的记录,通过这两个成员,可以知道是锁正在被哪个线程所持有。这里可以通过一个图来进行说明
而至于为何又会有nextC,那是因为可能存在着某种特殊的场景:
public void A(){
lock.lock();
xxxxx;
B();
lock.unlock();
}
public void B(){
lock.lock();
lock.unlock();
}
也就是说在执行A的之后,又会调用方法B来进行执行,那么对于此时,对于同一个线程来说,就会两次持有锁,那么这就是上面的nextc相加的原因了。
同理可以推断,syncronized也是可重入的
排队方法:
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)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
从这里可以看到,造就了一个CLH队列。Node节点中保存了对当前线程的引用
对于Node节点中的几个重要属性:
pre,next,waitstatus,thread,mode
首先来说一下:
mode表示的有两种形式:一种是独占也就是互斥,另外一个是共享;
waitstatus代表着当前节点的生命状态,有五种生命状态,分别代表着不同的情况
private Node enq(final Node node) {
// 所有的线程都得进来进行排队,不允许漏掉任何一个
// 不然底层JVM创建好的线程栈等等信息就会造成内存泄漏等情况
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;
}
}
}
}
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
这个方法转化一下:
// 当前进来的线程节点
acquireQueued(node, arg))
此时此刻代表的是进来排队的线程排好队之后,需要来进行阻塞了。
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;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在第一轮循环中,只是修改了前驱节点的状态,也仅仅只是做了一个修改而已,第二个节点及其后续的没有法神改变。表示的首结点可以被唤醒;第二轮循环才真正的表示的是可以被其进行阻塞,也就是把所有的线程都给阻塞掉了。
同时判断线程是否会有中断线程来进行唤醒的?
ReetrantLock是一个可以中断的锁
shouldParkAfterFailedAcquire(p, node)
在这个方法中,首先会将node的节点,也就是队首的节点的waitstatus从0->-1,这样做是因为想要在公平锁的情况下,当线程占有的锁被释放后,需要判断队首的节点的状态是否是-1,如果是-1,表示的可以被唤醒。
然后被唤醒后会再走一轮循环,然后拿到锁之后了再去进行执行。
但是这里可能存在着非公平的情况下,如果新来的线程和队首的线程同时来进行抢,那么这个时候会需要问题,位于队首的节点可能会再次进入到阻塞状态,那么再次把0修改成-1,放入到队列中来进行排列。再此经历过两轮循环,才能够重新恢复到-1状态