前言
ReentTrantLock其实在jdk1.6以前相比与synchronized效率上和api丰富性上是具有很大区别,
但是之后的话,其实性能上差距不大,但是在api的丰富性上ReentrantLock还是有有时
那为什么在1.6前synchronzed与ReentrantLock相比,性能上差很多呢?
- 首先当是的synchronzed在当时的需要同过操作系统的互斥量来实现锁,这就需要其在用户态和内核态上进行切换,当是直接上来就是重量级锁;但是在jdk1.6之后呢hotspot对synchronized进行的大量优化,通过锁的升级减少锁造成的开销,最后才到重量级锁
- ReentrantLock在其实线上使用了大量的cas,自旋,park…很大一部分减少了锁的开销,甚至在并发线程交替执行时,甚至可以忽略不计;
Lock()
情景: t1为第一个进入且很久不释放锁,t2随后进入然后被阻塞
t1进入后的过程:
进入ReentrantLoc的lock首先我们通过构造方法就能知道ReentrantLock是区分公平/非公平锁,对应的方法实现肯定也是不同的,ReentrantLock模式是非公平锁;
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
但是我们先使用公平锁来讲解,可以看到
进入FairSync的lock方法:
static final class FairSync extends Sync
final void lock() {
acquire(1);
}
仅仅又一个acquire方法:
/**
* 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) {
//如果没有获取到锁 !tryAcquire(arg)=true,才会执行下面的代码
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先得看if里面的表达式:如果 !tryAcquire(arg) 为true他才会执行下面的 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
tryAcquire这个方法会尝试去获取锁,现在让我们来看看这个尝试获取锁的代码(有多个实现,我们需要看FairSync):
FairSync:
/**
* 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();
//这个State就是ReentrantLock中aqs的一个state属性,默认是0,没被人持有
int c = getState();
if (c == 0) {
//如果没有被人持有
//!hasQueuedPredecessors():是否需要排队
if (!hasQueuedPredecessors() &&
//cas改变state的值
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,
只有其返回false,!hasQueuedPredecessors才是true,才会加下来的真正获取锁的一系列操作;
接下来我们看看hasQueuedPredecessors方法(为什么需要判断是否需要排队):
/**
* Queries whether any threads have been waiting to acquire longer
* than the current thread.
*
* <p>An invocation of this method is equivalent to (but may be
* more efficient than):
* <pre> {@code
* getFirstQueuedThread() != Thread.currentThread() &&
* hasQueuedThreads()}</pre>
*
* <p>Note that because cancellations due to interrupts and
* timeouts may occur at any time, a {@code true} return does not
* guarantee that some other thread will acquire before the current
* thread. Likewise, it is possible for another thread to win a
* race to enqueue after this method has returned {@code false},
* due to the queue being empty.
*
* <p>This method is designed to be used by a fair synchronizer to
* avoid <a href="AbstractQueuedSynchronizer#barging">barging</a>.
* Such a synchronizer's {@link #tryAcquire} method should return
* {@code false}, and its {@link #tryAcquireShared} method should
* return a negative value, if this method returns {@code true}
* (unless this is a reentrant acquire). For example, the {@code
* tryAcquire} method for a fair, reentrant, exclusive mode
* synchronizer might look like this:
*
* <pre> {@code
* protected boolean tryAcquire(int arg) {
* if (isHeldExclusively()) {
* // A reentrant acquire; increment hold count
* return true;
* } else if (hasQueuedPredecessors()) {
* return false;
* } else {
* // try to acquire normally
* }
* }}</pre>
*
* @return {@code true} if there is a queued thread preceding the
* current thread, and {@code false} if the current thread
* is at the head of the queue or the queue is empty
* @since 1.7
*/
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());
}
可以看到方法是AQS里面的方法,这就需要涉及到AQS里面的队列这个数据结构,这个数据结构主要就是:AQS的head和tail分别指向这个node链表的头和尾,node节点里面有pre和next指针,分别指向前后节点,还有一个thread属性,指向当是等待的线程;
return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
h!=t这个条件很重要,这里为true才会执行下面的条件;
就会因为很多种情况返回不同;我们此时是t1进入,此时aqs队列是为空,所以直接返回false(后续情况会在绕回这里,请大家注意这个方法,这里我先将t1)
从这里t1返回false ,那么接下来就直接cas设置state打断一系列操作,然后一层层返回,这样就上锁成功;
可以看到,如果没有锁的竞争,t1的上锁十分顺利,直接在jdk代码级别就就上锁了,连队列都没有入
如果两个线程交替执行的 reentrantLock是很快的,直接在代码层面上就解决了,不用到内核态,所以与当是的synchronized相比定然是快
那么此时t1如果一直持有锁,t2来了:
lock->tryacquire():
/**
* 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();
}
现在开始他与t2有竞争的关系了,那么我们一步步分析下
1:首先是尝试是否能获取锁
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
//此时t1还在一直持有锁
protected final boolean tryAcquire(int acquires) {
//t2
final Thread current = Thread.currentThread();
//1
int c = getState();
//false
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;
}
//返回false出去
return false;
}
}
- 我们可以看到返回false出去,那么!tryAcquire就是true了,其之后的条件方法就不会被短路
此时t2就得开始执行这里:acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
首先执行addWaiter(Node.EXCLUSIVE), arg):这个也是aqs里面的方法
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
//创建node,thread=currentThread
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
//t2执行
enq(node);
return node;
}
可以通过分析,t2得执行enq(node)方法
enq:
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
//t2
private Node enq(final Node node) {
//死循环(自旋)
for (;;) {
//获得尾节点
Node t = tail;
//尾节点为空
if (t == null) { // t2的第一次循环进入这里
//新建立过空的node,同时cas将head指向这个空node
if (compareAndSetHead(new Node()))
//将尾指针指向这个节点
tail = head;
} else {//t2的第二次循环进入这里,将自己维护进这个链表,同时返回方法出去
//如果不为空,直接一套设置:等于是将自己这个节点加入这个链表
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
//返回方法,跳出循环
return t;
}
}
}
}
此时 通过addWaiter 和 enq方法,t2初始化了aqs队列,同时自己也维护进去了;
那么此时就来到了acquireQueued(addWaiter(Node.EXCLUSIVE), arg)的acquireQueued方法
这个也是个aqs很重要的方法:
acquireQueued:
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
//标示
boolean failed = true;
try {
//中断标示
boolean interrupted = false;
//死循环(自旋)
for (;;) {
//p=获取到队列中的自己前一个节点
//此时因为就只有t2和一个空节点,所以t2的前一个节点:空节点-》head
///判断自己的上一个节点是不是头部,如果是,那就代表他是第二个,他就可以去尝试一下获取锁,因为可能在他入队的时候拿锁的线程释放了锁
final Node p = node.predecessor();
//前一个节点为head,返回true=》tryAcquire在尝试获取锁
if (p == head && tryAcquire(arg)) {
//如果此时t1释放,获取成功的话
//将自己设置为头节点,同时删除全部前面节点的引用让其被gc掉
//现在aqs队列中就只有自己这个node 同时node的thread也为null
//自己拿到了锁,自己应该出去这个队列,这样持有锁的那个线程永远不再队列
//将head指向自己,同时自己的thread=null,这样设计就能让持有锁的线程永远不参与排队
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//阻塞
//t1没释放锁,所以t2会执行下面,如果shouldParkAfterFailedAcquire为true就会被park中断掉,shouldParkAfterFailedAcquire很重要
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
setHead(将自己设置为头节点同时thread=null):
/**
* Sets head of queue to be node, thus dequeuing. Called only by
* acquire methods. Also nulls out unused fields for sake of GC
* and to suppress unnecessary signals and traversals.
*
* @param node the node
*/
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
shouldParkAfterFailedAcquire
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//t2进入,pre前一个节点为node
//ws是 waitterStatus,是当前线程的状态,从头到尾默认都是没有给这个传过值,ws=0
int ws = pred.waitStatus;
//第一次:ws=0(默认)第二次进入:ws=-1
//第一次:false
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.
*/
//改变waitStatus=Node.SIGNAL=-1
//为什么要帮上一个节点改为-1?
//可以在自旋一次同时帮上一个节点将状态改为休眠-1,同时-1代表当前节点的后一个节点在排队
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//第一次由于waitstatus=0,直接返回false
return false;
}
可以看到第一次进入因为ws变量默认为0,所以执行完cas改变ws为signal(-1)后就返回false
这样返回到shouldParkAfterFailedAcquire中再看看:
inal 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;
}
//第一次shouldParkAfterFailedAcquire返回false,短路
//接下来直接执行下一次for循环(for在自旋一次)
//接下来如果再次获取锁没有成功在进入这个shouldParkAfterFailedAcquire方法中,由于上次改成了SIGNAL所以返回true,直接进入parkAndCheckInterrupt进行park线程阻塞了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这样总结:t2由于t1一直没释放锁:首先会进行队列初始化,然后在判断上一个节点是否为head,如果是还可以在获取一次锁,如果这次没获取成功,在进入shouldParkAfterFailedAcquire中ws变量的值默认为0的问题上会再次让for循环自旋,从而在进行tryAcquire一次;所以如果当前线程是第一个被阻塞的就会自旋尝试获取锁两次
同时又一个问题,为什么要初始化一个thread为null的空头节点?同时shouldParkAfterFailedAcquire在这里又帮自己的上一个节点的waitstate节点改为SIGNAL(-1)?
我的理解:其实一直都会有一个thread为null的在第一个,同时如果自己出队后也会将前一个给踢开,将head指向自己,同时自己的thread=null,这样设计就能让持有锁的线程永远不参与排队,同时这个null的节点设计,可以理解为当切排队线程知道前面又线程拿着锁,但是不知道是谁
帮上一个节点将状态改为休眠-1,同时-1代表当前节点的后一个节点在排队
明天早补完非公平的情况?