Java并发4:ReentrantLock原理

前言

主要用到AQS的并发工具类:
在这里插入图片描述
ReentrantLock和AQS的关系
在这里插入图片描述

  • ReentrantLock 在内部用了内部类 Sync 来管理锁,所以真正的获取锁和释放锁是由 Sync 的实现类来控制的。
  • Sync 有两个实现,分别为 NonfairSync(非公平锁)和 FairSync(公平锁)。
  • 默认是非公平锁
public ReentrantLock() {
	sync = new NonfairSync();
}

1、非公平锁实现原理

加锁流程

没有竞争时:
在这里插入图片描述
第一个竞争出现时:
在这里插入图片描述
Thread-1执行了:

  1. CAS尝试将state由0改为1,结果失败

  2. 执行acquire(1);进入tryAcquire(),此时state已经是1且当前owner并不是Thread-1本身,结果false;

  3. 进入addWaiter,构造Node队列

    • Node的创建是懒惰的
    • 第一个Node成为Dummy(哑元)或哨兵,用来占位不关联线程。
      在这里插入图片描述
  4. Thread-1进入acquireQueued逻辑

  • 在一个死循环中不断尝试获得锁,失败后进入park阻塞
  • 如果自己是head的next,则再次tryAcquire尝试获得锁。这是state仍然为1,获取锁失败
  • 进入shouldParkAfterFailedAcquire,发现前驱节点head的waitStatus不是SIGNAL,compareAndSetWaitStatus将head的waitStatus设置为-1;
    在这里插入图片描述
  • 回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时state 仍为 1,失败。
  • 再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回true
  • 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
    在这里插入图片描述
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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

若是多个线程经历上述失败过程:
在这里插入图片描述

Thread-0释放锁,进入tryRelease,若成功:

  • 设置 exclusiveOwnerThread 为 null
  • state = 0
    在这里插入图片描述
    当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程
    找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1
    回到 Thread-1 的 acquireQueued 流程
    在这里插入图片描述
    如果加锁成功(没有竞争),会设置
  • exclusiveOwnerThread 为 Thread-1,state = 1
  • head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
  • 原本的 head 因为从链表断开,而可被垃圾回收

如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了
在这里插入图片描述
如果不巧又被 Thread-4 占了先

  • Thread-4 被设置为 exclusiveOwnerThread,state = 1
  • Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞

加锁源代码汇总

static final class NonfairSync extends Sync {
     final void lock() {
     	//首先使用cas尝试将state从0改为1(仅尝试一次),若成功表示获取独占锁
          if (compareAndSetState(0, 1))
              setExclusiveOwnerThread(Thread.currentThread());
          else
              acquire(1);
      }
}

//AQS继承的方法
public final void acquire(int arg) {
      if (!tryAcquire(arg) &&
      	  // 当 tryAcquire 返回为 false 时, 先调用 addWaiter ㈣, 接着 acquireQueued ㈤
          acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
          selfInterrupt();
  }

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

//Sync继承的方法
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果还没有其他进程获得锁
    if (c == 0) {
    	//尝试cas获取锁,这里体现了非公平性:不去检查AQS队列
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //发生了锁重入
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;//state++;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

//AQS继承的方法
private Node addWaiter(Node mode) {
	//根据当前线程新建一个Node节点,模式为独占模式
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    //若tail不为空,cas尝试将Node对象假如AQS队列尾部
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

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;
            }
        }
    }
}


final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 上一个节点是 head, 表示轮到自己(当前线程对应的 node)了, 尝试获取
            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);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    //前驱结点的waitStatus已经设置为SIGNAL,当前线程可以放心地park了
    if (ws == Node.SIGNAL)
        return true;
    //进入阻塞队列排队的线程会被挂起,而唤醒的操作是由前驱节点完成的。
        // 所以下面这块代码说的是将当前节点的prev指向waitStatus<=0的节点,
        // 简单说,就是为了找个好爹,因为你还得依赖它来唤醒呢,如果前驱节点取消了排队,
        // 找前驱节点的前驱节点做爹,往前遍历总能找到一个好爹的
    if (ws > 0) {
       
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
//负责挂起线程的
// 这里用了LockSupport.park(this)来挂起线程,然后就停在这里了,等待被唤醒
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

解锁流程

public void unlock() {
    sync.release(1);
}
//继承自AQS
public final boolean release(int arg) {
	//尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        //队列不空且waitStatus==SIGNAL才需要unpark
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //支持锁重入,只有state减为0,才释放成功
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    //若是状态为SIGNAL,尝试重置状态为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    //从前往后找到未取消的节点
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

2、可重入原理

  • 加锁过程中,若判断当前线程已经获得了锁,表示发生了锁重入
  • 解锁过程中,将state-1,只有state减为0才释放成功

3、公平锁实现原理

static final class FairSync extends Sync {
	private static final long serialVersionUID = -3000897897090466540L;
	final void lock() {
		acquire(1);
	}
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
		selfInterrupt();
	}
}

// 与非公平锁主要区别在于 tryAcquire 方法的实现
protected final boolean tryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		// 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
		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;
}
// ㈠ AQS 继承
public final boolean hasQueuedPredecessors() {
	Node t = tail;
	Node h = head;
	Node s;
	// h != t 时表示队列中有 Node
	return h != t &&((s = h.next) == null ||s.thread != Thread.currentThread());
}

4、条件变量实现原理

每个条件变量对应着一个等待队列,实现类是ConditionObject
等待条件的过程

  1. 在操作条件队列之前需要获得独占锁,不然直接在获取独占锁的时候就被挂起了
  2. 成功获取独占锁后,若是当前条件不满足,则在当前锁的条件队列上挂起,同时释放获取的锁资源
  3. 如果被唤醒,检查是否可以获取独占锁1,斗则继续挂起
    条件满足后的唤醒过程
  4. 把当前等待队列的第一个有效节点加入同步队列等待被前置节点唤醒,若此时前置节点被取消则直接唤醒该节点让它重新在同步队列中适当的尝试获取锁或者挂起

await流程

1、 开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程
创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部
在这里插入图片描述
2、进入fullyRelease,释放同步器上的锁

在这里插入图片描述
3、unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功
在这里插入图片描述
4、park阻塞Thread-0
在这里插入图片描述

signal流程

假设Thread-1要来唤醒Thread-0

  • 进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node
    在这里插入图片描述
  • 执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的
    waitStatus 改为 -1
    在这里插入图片描述
  • Thread-1 释放锁,进入 unlock 流程,略

源码

1、将节点加入到条件队列

// 首先,这个方法是可被中断的,不可被中断的是另一个方法 awaitUninterruptibly()
// 这个方法会阻塞,直到调用 signal 方法(指 signal() 和 signalAll(),下同),或被中断
public final void await() throws InterruptedException {
    // 老规矩,既然该方法要响应中断,那么在最开始就判断中断状态
    if (Thread.interrupted())
        throw new InterruptedException();

    // 添加到 condition 的条件队列中
    Node node = addConditionWaiter();

    // 释放锁,返回值是释放锁之前的 state 值
    // await() 之前,当前线程是必须持有锁的,这里肯定要释放掉
    int savedState = fullyRelease(node);

    int interruptMode = 0;
    // 这里退出循环有两种情况,之后再仔细分析
    // 1\. isOnSyncQueue(node) 返回 true,即当前 node 已经转移到阻塞队列了
    // 2\. checkInterruptWhileWaiting(node) != 0 会到 break,然后退出循环,代表的是线程中断
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 被唤醒后,将进入阻塞队列,等待获取锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    //删除条件队列中被取消的节点
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}



private Node addConditionWaiter() {
     Node t = lastWaiter;
     //如果最后一个节点被取消,则删除队列中被取消的节点
     //至于为啥是最后一个节点后面会分析
     if (t != null && t.waitStatus != Node.CONDITION) {
         //删除所有被取消的节点
         unlinkCancelledWaiters();
         t = lastWaiter;
     }
     //创建一个类型为CONDITION的节点并加入队列,由于在临界区,所以这里不用并发控制
     Node node = new Node(Thread.currentThread(), Node.CONDITION);
     if (t == null)
         firstWaiter = node;
     else
         t.nextWaiter = node;
     lastWaiter = node;
     return node;
 }

//删除取消节点的逻辑虽然长,但比较简单,就不单独说了,就是链表删除
 private void unlinkCancelledWaiters() {
     Node t = firstWaiter;
     Node trail = null;
     while (t != null) {
         Node next = t.nextWaiter;
         if (t.waitStatus != Node.CONDITION) {
             t.nextWaiter = null;
             if (trail == null)
                 firstWaiter = next;
             else
                 trail.nextWaiter = next;
             if (next == null)
                 lastWaiter = trail;
         }
         else
             trail = t;
         t = next;
     }
 }

2、完全释放独占锁

如果一个线程在不持有 lock 的基础上,就去调用 condition1.await() 方法,它能进入条件队列,但是在上面的这个方法中,由于它不持有锁,release(savedState) 这个方法肯定要返回 false,进入到异常分支,然后进入 finally 块设置 node.waitStatus = Node.CANCELLED,这个已经入队的节点之后会被后继的节点”请出去“。

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        // 这里使用了当前的 state 作为 release 的参数,也就是完全释放掉锁,将 state 置为 0
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

3、等待加入阻塞队列

int interruptMode = 0;
// 如果不在阻塞队列中,注意了,是阻塞队列
while (!isOnSyncQueue(node)) {
    // 线程挂起
    LockSupport.park(this);

    // 这里可以先不用看了,等看到它什么时候被 unpark 再说
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
}

// 在节点入条件队列的时候,初始化时设置了 waitStatus = Node.CONDITION
// 前面我提到,signal 的时候需要将节点从条件队列移到阻塞队列,
// 这个方法就是判断 node 是否已经移动到阻塞队列了
final boolean isOnSyncQueue(Node node) {

    // 移动过去的时候,node 的 waitStatus 会置为 0,这个之后在说 signal 方法的时候会说到
    // 如果 waitStatus 还是 Node.CONDITION,也就是 -2,那肯定就是还在条件队列中
    // 如果 node 的前驱 prev 指向还是 null,说明肯定没有在 阻塞队列(prev是阻塞队列链表中使用的)
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // 如果 node 已经有后继节点 next 的时候,那肯定是在阻塞队列了
    if (node.next != null) 
        return true;

    // 下面这个方法从阻塞队列的队尾开始从后往前遍历找,如果找到相等的,说明在阻塞队列,否则就是不在阻塞队列

    // 可以通过判断 node.prev() != null 来推断出 node 在阻塞队列吗?答案是:不能。
    // 这个可以看上篇 AQS 的入队方法,首先设置的是 node.prev 指向 tail,
    // 然后是 CAS 操作将自己设置为新的 tail,可是这次的 CAS 是可能失败的。

    return findNodeFromTail(node);
}

// 从阻塞队列的队尾往前遍历,如果找到,返回 true
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

4、signal唤醒线程,转移到阻塞队列
刚刚到 LockSupport.park(this); 把线程挂起了,等待唤醒。
唤醒操作通常由另一个线程来操作,就像生产者-消费者模式中,如果线程因为等待消费而挂起,那么当生产者生产了一个东西后,会调用 signal 唤醒正在等待的线程来消费。

// 唤醒等待了最久的线程
// 其实就是,将这个线程对应的 node 从条件队列转移到阻塞队列
public final void signal() {
    // 调用 signal 方法的线程必须持有当前的独占锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

// 从条件队列队头往后遍历,找出第一个需要转移的 node
// 因为前面我们说过,有些线程会取消排队,但是可能还在队列中
private void doSignal(Node first) {
    do {
          // 将 firstWaiter 指向 first 节点后面的第一个,因为 first 节点马上要离开了
        // 如果将 first 移除后,后面没有节点在等待了,那么需要将 lastWaiter 置为 null
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        // 因为 first 马上要被移到阻塞队列了,和条件队列的链接关系在这里断掉
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
      // 这里 while 循环,如果 first 转移不成功,那么选择 first 后面的第一个节点进行转移,依此类推
}

// 将节点从条件队列转移到阻塞队列
// true 代表成功转移
// false 代表在 signal 之前,节点已经取消了
final boolean transferForSignal(Node node) {

    // CAS 如果失败,说明此 node 的 waitStatus 已不是 Node.CONDITION,说明节点已经取消,
    // 既然已经取消,也就不需要转移了,方法返回,转移后面一个节点
    // 否则,将 waitStatus 置为 0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // enq(node): 自旋进入阻塞队列的队尾
    // 注意,这里的返回值 p 是 node 在阻塞队列的前驱节点
    Node p = enq(node);
    int ws = p.waitStatus;
    // ws > 0 说明 node 在阻塞队列中的前驱节点取消了等待锁,直接唤醒 node 对应的线程。唤醒之后会怎么样,后面再解释
    // 如果 ws <= 0, 那么 compareAndSetWaitStatus 将会被调用,上篇介绍的时候说过,节点入队后,需要把前驱节点的状态设为 Node.SIGNAL(-1)
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 如果前驱节点取消或者 CAS 失败,会进到这里唤醒线程,之后的操作看下一节
        LockSupport.unpark(node.thread);
    return true;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值