声明锁对象,构造函数默认不传是创建非公平锁,传true是创建一个公平锁
ReentrantLock lock = new ReentrantLock(true);
重入锁,lock和unlock成对出现,
lock.lock();
lock.unlock();
尝试加锁,有返回值的加锁成功返回true,失败则
lock.tryLock();
锁阻塞期间,可以掉用线程的interrupt打断,然后用InterruptedException捕获来中断锁阻塞的状态,
lock.lockInterruptibly();
InterruptedException
myThread1.interrupt();
了解过ReentrantLock里面有一个AQS(AbstractQueuedSynchronizer),抽象的队列同步的? 用它来实现的加锁解锁,这两天看了一点源码,做个笔记.
在看源码之前,首先猜测aqs应该就是一个队列,因为它有公平锁一说,所以必然队列是先进先出的,然后应该是当前对象获取到锁之后,后续进来的会去队列里排队等待,解锁之后,取出队列的第一个进行唤醒.
下面部分直接说结论了,不循序渐进了
首先看公平锁的实现,非公平锁相对还要简单一些.
我看下来,觉得公平锁的核心部分就是这个函数,实际上这里进行了3个比较重要的工作和1个不那么重要的工作,
返回false代表取锁失败,有几种可能
简单说下代码流程
这个函数是获取锁的主要部分,而且后续也在其他流程里多次调用了这个函数,
state是当前锁状态, =0代表无锁,=1代表有锁
如果无锁状态下,会进入队列判断,也就是hasQueuedPredecessors
这个地方进行了多次的非判断,很干扰阅读的逻辑,
首先说明,AQS会单独存储队列中的头尾节点,tail和head.
首先头尾相等只有一种可能,就是两个都是null,所以用第一个判断来确定,如果都是null则没有正在跑的队列,
第二个是判断当前线程是不是head.next. 如果它不是则没有资格继续拿锁,直接返回false,如果是,才进入往下判断.
compareAndSetState() //更新状态,不说了,
setExclusiveOwnerThread() //设置当前拿锁线程,
下面这部分是判断重入锁的部分,如果当前线程就是拿到了锁的线程,则再次进入,并且State+=1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
至此,tryAcquire部分结束,主要就是尝试加锁,true成功,false失败,
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
回到acquire,tryAcquire返回值为true,并且这里是"非"true的话,也就是说,返回true,就直接整个判断退出了(这里有点绕),如果是返回false,则进入下面一个函数,按照执行逻辑,先进入addWaiter.
AQS中的Q是个双向链表,其中的节点就是这个Node,结构为:waitStatus/prev/next/thread
刚才说了,会单独存储头尾节点,此时尝试取出尾结点,原则上新进来的线程需要加到队列的最尾部分,之前的尾步成为倒数第二,
如果尾结点==null,则会进入enq进行队列初始化
死循环来保证,一定是有头尾节点后,才返回
逻辑稍微有点绕,简单的说
这有个比较怪的地方,
读代码,很容易误解成 t.next = node 是在把尾节点的next指定成当前节点,但实际上这个t是之前的head节点的引用,真正的尾节点已经在compareAndSetTail中更新成了当前节点.
为什么<<尾=头>>,而且下面还用了unsafe的方法,不能直接用两个引用分别做事情吗…
至此addWaiter结束,此时完成了对当前线程的节点封装,并且保证了队列当中有头尾节点.
把刚才包装好的node加到队列里,
这里也是一个死循环,
先是取出当前节点的前一个,判断前一个是不是头结点,如果是,则再次调用tryAcquire来尝试获取锁资源,如果此时成功了,会更新节点信息后,直接放回,相当于拿到了锁,相当于一次自旋,
如果失败则进入第二个if,
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
上面代码由于源码中注释太多,没有用截图,直接把注释删掉贴了过来,
有一个需要注意的是,这里判断的主体都是以当前节点的前一个,如果前一个是SIGNAL(这理解不是很透彻,直接贴一段注释)
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
如果是SIGNAL则返回true,当前线程进入park
如果不是SIGNAL,则会进入到下面的else里,把上一个设置成SIGNAL,待死循环的下一次执行进来直接返回true.
逻辑猜测:
估计就是取当前队列的第一个,进行唤醒,应该比加锁过程简单的多
直接看码
上图是核心部分,“尝试解锁”,“尝试成功”,则取到head,进行unpark唤醒
取到head,如果head的waitStatus>0则不需要唤醒任何,<0则设置成0,并且走唤醒逻辑
取到头的next节点,判断非空,或者等待状态>0,正常情况下它应该是-1的,这种是排除队列中有其他的异常情况,注释中写道<<要解锁的通常是h的下一个节点,但如果出现了明显的null,或状态取消,则从尾部开始向前便利.>>
一个for循环,不断从后向前便利,改变目标节点的值,最终取到的目标节点就是,在队列里面第一个不为空且状态是对的,
拿到节点,进行unpark
tryLock,直接调用了非公平锁的,请求锁方法tryAcquire,并且直接把tryAcquire的返回值返回了.相当于没有取到锁的话,没有后续的排队环节,直接返回false了.
lockInterruptibly,等待锁的过程中,可以调用线程的Interrupt进行打断,然后用一个异常捕捉到,然后让线程继续做别的事情.核心在于这个地方直接抛出了异常,
一会会画一个流程图,走一遍流程,
代码里有两个我不太喜欢的地方
1:有太多的双非判断,不好理解,
2:执行逻辑和判断逻辑是结合在一起的,我一般会尽量的把执行和判断分开,哪怕代码执行效率会略微下降一点,尽量避免在if括号里进行大量的工作执行.