本文纯属本人这两天看reentranlock所做的笔记,有什么不对的地方,可以指教指教
在介绍rentranlock之前,我们先认识下locksupport.park和locksupport.unpark,下面将写个代码:
public class mylock {
public static void main(String[] args) {
Thread t1=new Thread(()->{
LockSupport.park();
System.out.println(111);
},"t1");
t1.start();
LockSupport.unpark(t1);
}
}
执行结果是111;
当我们将LockSupport.unpark(t1);去掉的话,那么线程将一直阻塞在那里,程序不会结束;
当调用park()方法会把当前线程让出cpu资源 其会有一下四种方式被唤醒 1、unpark() 2、interrupt() 3、未知原因 (源码里就是这么写的 for no reason) 4、规定的时间被唤醒 理解了park和upark下面我们就来看看reentranlock源码,先谈谈公平锁
reentranlock就是通过park和unpark的方法实现锁的同步,除此之外,这里还维护了一个aqs队列,这个队列是一个双端队列,里面维护着对头和队尾还有一个state表示锁的状态
aqs队列主要包括:
private Node head;
private Node tail;
private Volitile state; 表示锁的状态 state>1表示重入锁
Node节点主要包括:
/** 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; //表示该节点处于可唤醒状态 可以调用park方法这是由后面一个节点设置的 后面会说
/** 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 Node next; //后置节点
volatile Thread thread; //当前线程
看了这幅图,大家应该会对这个队列中为什么第二个节点时空会产生疑问,我们下面会解释。
公平锁加锁过程:
public final void acquire(int arg) {
//1、当第一个线程过来时,直接获得锁,不需要初始化aqs队列
//2、当发生说竞争tryAcquire就会返回false 继而初始化aqs队列,将当前线程加入到队列中
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先调用tryAcquire方法 进行尝试加锁,如果加锁成功,返回true不用往下执行,如果失败,则执行
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
;
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;
}
}
要想成功获取锁hasQueuedPredecessors
方法的返回false,其条件是:1、当第一个线程进入h != t
返回false;2、当锁持有者的线程释放了,如果队列里还有节点,那么就会看头节点的之后还有没有节点
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());
}
此操作是将当前线程节点加入到队尾
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) { //判断队尾是不是为空,如果不为空,则通过CAS将节点添加到队尾
node.prev = pred; //将尾节点作为创造出来的节点的前一个节点,即将node链接到为节点后
/**
* 基于CAS将node设置为尾节点,如果设置失败,说明在当前线程获取尾节点到现在这段过程中已经有其他线程将尾节点给替换过了
* 注意:假设有链表node1-->node2-->pred(此处是双向链表),
* 通过CAS将pred替换成了node节点,即当下的链表为node1-->node2-->node,
* 然后根据上边的"node.prev = pred"与下边的"pred.next =node"将pred插入到双链表中去,组成最终的链表如下:
* node1-->node2-->pred-->node
* 这样的话,其实我们并没有指定node2.next=pred与pred.prev=node2,这是为什么呢?
* 因为在之前这两句就早就执行好了,即node2.next和pred.prev这连个属性之前就设置好了
*/
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);//当前队列为空,则将当前节点加入到aqs队列
return node;
}
下面就解释了为什么在aqs队列中,队列得第二个节点的线程是空,第三个节点才是真正的线程
大概得流程 就是如果tail是null,就新建一个Node,然后CAS置换成新的Head之后,把head给tail,
初始化就完成了,然后for循环,node.prev = t;compareAndSetTail(t, node)
,队列变成head->node(双向链表),再调用t.next = node;
;链表变成head->tail->node;
而这tail是等于head的;
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;
}
}
}
}
入队成功后调用acquireQueued
方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//无限循环(一直阻塞),直到node的前驱节点p之前的所有节点都执行完毕,
//p成为了head且node请求成功了
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
//这会将获取到锁的线程设置为Head 也就是空节点,当前线程就被这位null
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//当线程获取不到锁,就会执行shouldParkAfterFailedAcquire将前一个线程的节点的waitStatus设置成-1(这是一种状态0表示无锁,-1表示可唤醒状态),设置成功后就执行parkAndCheckInterrupt将当前线程挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //将当前线程挂起 释放cou资源 等待被唤醒
return Thread.interrupted();
}
非公平锁的加锁过程:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
我们发现非公锁就比公平锁就多了一个cas操作,也就是说当有线程进来时,该线程可以直接去竞争这把锁,而不需要排队,如果竞争失败,再去走公平锁的流程。
总结
1、公平锁线程获取锁,获取不到会进AQS队列
2、线程进入队列会将当前node节点前一个节点的waitstatus状态置为可唤醒状态
3、非公平锁加锁其实和公所锁加锁实现差不多,唯一的区别就是公平锁加锁时,进来的线程可以直接参与锁的竞争,如果竞争失败,会执行公平锁的逻辑
那公平锁和非公平锁谁的效率高呢?有兴趣的可以想想