当前位置 : 主页 > 编程语言 > java >

Java-ReentrantLock-NonfairSync/FairSync

来源:互联网 收集:自由互联 发布时间:2022-07-13
本文都是根据我自己阅读源码和自己的理解记录的,仅供参考 在了解ReentrantLock原理之前,请必须要了解java的CAS,也叫compare and swap,既比较和修改,java中,在java.util.concurrent.atomic包下


本文都是根据我自己阅读源码和自己的理解记录的,仅供参考

在了解ReentrantLock原理之前,请必须要了解java的CAS,也叫compare and swap,既比较和修改,java中,在java.util.concurrent.atomic包下的基本都是实现了CAS,它们的方法命名规则基本都以compareAnd开头,例如AtomicReference类中的compareAndSet方法,不过在java中CAS的最底层实现是通过Unsafe类,而Unsafe类的compareAndSwap方法是native方法,用C++实现的,在java11中我已经找不到Unsafe这个类了,但是并不影响ReentrantLock的原理

与synchronized关键字的区别,下面这句话是doug lea和joshua bloch说的,够权威吧"在内部所不能够满足使用时,ReentranLock才被视作更高级的工具,当你需要以下特性的时候,才应该使用:可定时的,可轮询的,或者可中断的锁操作,公平队列,或者非块状结构的锁,否则,请使用synchronized"

日记:synchronized是内部锁,内置于JVM,而ReentranLock是JNI方法,C++实现,本质上依赖操作系统,言外之意就是,后续JDK优化的时候,会优化JVM,而不会优化操作系统,从JDK6开始,synchronized性能已经完全不弱于ReentranLock,强烈推荐不是特殊需求请使用synchronized关键字

第一部分:NonfairSync

首先,必须要了解一些字段,这样才能读代码

public class ReentrantLock{
/**
*表示当前持有锁的线程,如果没有线程持有锁,则是null
*当前线程持有锁之后,当前线程会修改该字段
*当前线程释放锁之后,当前线程会修改该字段
**/
private transient Thread exclusiveOwnerThread;
/**
*如果没有线程持有锁,则state=0,如果一个线程T1获取了锁,则T1对该值进行加1
*如果T1再次获得了锁,则T1再次进行加1
*当T1释放了一次锁(unlock方法),则该值减1,以此类推
*也就是说:T1 lock了多少次,就必须unlock多少次,否则其他线程无法获取锁
**/
private volatile int state;
}public abstract class AbstractQueuedSynchronizer{
/**
* Node是对线程的封装,哪个线程持有锁,那么封装这个线程的Node就是head
* 也就是说:head里装的是当前持有锁的线程
* 奇怪的是源码注释中没有写这个特性
*/
private transient volatile Node head;
}static final class Node {
// 1,说明当前节点取消了等待,比如lock.tryLock(time, unit)方法超时,以后也不会再次阻塞
// -1,则说明下一个节点应该unpark或者unlock(解除挂起状态)
// -2,我不清楚 TODO TODO
// -3,我不清楚 TODO TODO
volatile int waitStatus;
}

1:lock()
1.initializeSyncQueue方法(如果没获取到锁会走该方法)
初始化AbstractQueuedSynchronizer中的字段head和字段tail,赋同一个值

private final void initializeSyncQueue() {
Node h;
if (HEAD.compareAndSet(this, null, (h = new Node())))
tail = h;
}

上面的方法,在不考虑并发的情况下,等于下面的方法,这样会更好理解

private final void initializeSyncQueue() {
Node h = new Node();
if(this.head == null){
head = h;
tail = h;
}
}

2.addWaiter方法(如果没获取到锁会走该方法)
如果mode为空,在双向列表添加1个节点,并返回倒数第1个(尾部)节点
如果mode不为空,在双向列表添加2个节点,返回倒数第2个节点

private Node addWaiter(Node mode) {
// 创建2个新的节点,这个新的节点里保存了当前线程
// 注意这个构造方法,将mode放到node的后面(node-->mode)
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
// 这个if里的内容是:
// 将新的节点node放到尾部tail,然后再将原来的tail的next
// 指向这个新节点node,说白了就是在双向链表上增加2(或者1)个节点,具体是1还是2取决于入参mode是不是空,如果是空则增加1个节点(只有node),如果不是空则增加2个节点(node和mode)
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
//刚进for循环先走这个,后走if
initializeSyncQueue();
}
}
}

3.tryAcquire方法
这没什么说的,直接调用nonfairTryAcquire方法

protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}

4.nonfairTryAcquire方法
当前线程尝试获取锁,该方法是可重入的实现
如果此时没有其他线程占有锁,或者,当前占有锁的线程就是当前线程
那么就返回true,表明获取到锁
否则返回false,表示没有获取到锁

final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 拿到state状态
int c = getState();
// 如果是0说明没有其他线程持有锁,则当前线程有机会对其赋值
if (c == 0) {
// 如果赋值成功,则表示当前线程持有锁(此处采用CAS),
// 对state加1,表示当前线程第一次持有锁,注意acquires=1
if (compareAndSetState(0, acquires)) {
// 将当前线程赋值到exclusiveOwnerThread字段
// 其他地方可以通过该字段判断是哪个线程在持有锁
setExclusiveOwnerThread(current);
// 返回true,表示加锁成功
return true;
}
}
// 如果不是0,则说明有线程占用锁了,那么判断占有锁的线程是不是
// 当前线程,如果是,则可重入(Reentran)
else if (current == getExclusiveOwnerThread()) {
// 因为再次持有锁(重入进来的),所以此处对state赋值
// state是几,就表示当前是第几次再次获得锁
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果以上都不是,则没有获取到锁
return false;
}

5.shouldParkAfterFailedAcquire方法(如果没获取到锁会走该方法)
这个方法不是很懂

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 如果ws=负1,表示这个节点后面的节点应该处于等待状态
if (ws == Node.SIGNAL){
return true;
}
if (ws > 0) {
// 好遗憾,没看懂
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 好遗憾,没看懂
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}

6.acquireQueued方法

final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
// 如果node是第2个节点,并且当前线程持有锁
// 那么就将node变成第1个节点(head节点),原head直接GC回收掉
// 通俗的说就是队列中每个节点向前挪动1个位置
// NOTE:不要忘了文章开头head节点是做什么的
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
// 如果没获取到锁,则调用LockSupport.park方法挂起,并放到队列的最后一个(tail)位置
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}

2:unlock()
NonfairSync与FairSync的unlock是相同的

1.ReentrantLock.Sync.tryRelease(int releases)方法

// 该方法很简单,就是当前线程释放锁,state=0就表明释放成功,返回true
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

2.unparkSuccessor方法

private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果小于0,就修改成0,表示当前节点什么状态都没有,就是一个普通节点
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
//获取下一个节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从队列最后一个节点开始遍历,也就是说从尾向头遍历,找到waitStatus<=0并且还离头最近的节点,给s
// 日记:此处我有个疑问,为啥不从头遍历?遇见<=0就跳出不就完事了吗
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
//如果没有走上面的if,则说明s的ws=1,表示取消了等待(unpark)
//如果走上面的if,那么请看上面的if注释
//将下一个节点解除挂起状态(unpark)
if (s != null)
LockSupport.unpark(s.thread);
}

第二部分:FairSync
FairSync与NonfairSync本质的不同就是在于tryAcquire方法,下面是FairSync的tryAcquire的定义,但是对于该类,我依然有些地方还是有一些疑惑的,所以此篇文章只供参考

1.AbstractQueuedSynchronizer.hasQueuedPredecessors()方法
判断当前线程所在的节点前面的节点,是否有其他等待的线程,
如果有其他等待线程,返回true
如果没有其他等待线程,返回false

public final boolean hasQueuedPredecessors() {
Node h, s;
// 如果头节点不为空
if ((h = head) != null) {
// 如果第2个节点为空,或者第2个节点不为空但是取消了等待(>0)
if ((s = h.next) == null || s.waitStatus > 0) {
s = null; // traverse in case of concurrent cancellation
//从尾部向头部遍历,找到离头部最近的,并且waitStatus<=0的节点,用s保存这个节点
for (Node p = tail; p != h && p != null; p = p.prev) {
if (p.waitStatus <= 0)
s = p;
}
}
// 如果s不是空并且s中的线程不是当前线程,则说明s中保存的线程正在等待获取锁,所以返回true,表示有其他线程等待
if (s != null && s.thread != Thread.currentThread())
return true;
}
// 如果头节点为空,说明队列里没任何线程,进一步说明没有其他线程在等待,直接返回false
return false;
}

那么由此可以看出,FairSync与NonFairSync的本质区别,在于获取锁的时候

FairSync会绅士礼貌一点,如果已经有其他节点(或者说线程)已经在等待了,它就会默默的将当前线程放到队列的尾部

NonfairSync会自私一点,直接开始插队,能插到的话,就或得到锁,如果插队失败,在将当前线程放到队列的尾部


网友评论