ReentrantLock之lock、unlock

1 篇文章 0 订阅

类图

类图

非公平锁实现原理

ReentrantLock默认使用的是非公平锁(NonfairSync)

构造器

先从构造器开始,默认为非公平锁实现

public ReentrantLock() {
    sync = new NonfairSync();
}

加锁流程

// ReentrantLock
public void lock() {
    sync.lock();
}

ReentrantLock的lock方法直接调用的Sync的lock方法

加锁成功

//NonfairSync
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

可以看到利用CAS进行来争抢,如果获取成功则设置独占标记。如果失败,就执行acquire方法。

//AbstractQueuedSynchronizer
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
//NonfairSync
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
//NonfairSync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

acquire方法会首先会执行tryAcquire方法,最终会执行nonfairTryAcquire方法。nonfairTryAcquire方法中仍然会先获取state的值,如果为0,说明锁被释放了,可以继续争抢。如果成功,则设置标记后返回。state不为0时,说明锁仍被占有,但需要继续判断锁的持有者,如果是该线程自己,则可以继续获取锁。这也是ReentrantLock被称为可重入锁的原因。

加锁失败

利用CAS尝试将state从0改为1,如果失败,则执行acquire方法。先回顾一下acquire方法。

//AbstractQueuedSynchronizer
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

首先会执行tryAcquire,再次失败后就执行acquireQueued方法。acquireQueued第一个实参是通过addWaiter方法返回的。

addWaiter
//AbstractQueuedSynchronizer
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) {
        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;
            }
        }
    }
}

传进来的默认的值是Node.EXCLUSIVE,使用的是排它模式。然后创建一个Node关联当前线程,并设置mode。首先会判断tail是否为null,如果是则表明该线程是第一个来排队的,使用懒加载初始化head,值为null,用尾插法并在CAS的帮助下将node放在head的后面。接下来就可以看acquireQueued方法了。

acquireQueued
//AbstractQueuedSynchronizer
final 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;
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

方法中使用了死循环。首先判断node(关联了当前线程)的前驱节点是否是head,然后再次尝试获取锁,获取成功后就出队。否则执行shouldParkAfterFailedAcquire方法。

shouldParkAfterFailedAcquire

首先我们要知道为了编码的方便Node与Thread是相互关联的,可以把Node当作当前线程。然后在看方法之前,我们需要先了解另一个知识点:Node的waitStatus值。

static final class Node {
	/** 当前线程被取消 */
    static final int CANCELLED =  1;
    /** 后继线程需要被当前线程唤醒 */
    static final int SIGNAL    = -1;
    /** 当前线程处于等待状态,等待条件的发生 */
    static final int CONDITION = -2;
    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */
    static final int PROPAGATE = -3;

    /**
     * status的取值:
     *   SIGNAL:     表示此节点的后继节点被(或即将被)阻塞(通过park方法),
     *				 因此当前节点在释放或取消时必须唤醒其后继节点。
     *     			 为了避免竞争,获取方法必须首先指示它们需要一个信号,然后重试原子获取,
     *         		 如果失败则阻塞。			 
     *   CANCELLED:  表示由于超时或中断,此节点被取消。
     *				 处于该状态的节点不会再变为其它状态。
     *               特别地,具有被取消节点的线程不会再次被阻塞。
     *   CONDITION:  此节点当前位于条件队列中。
     *               直到转移时,它将不会被用作同步队列节点,
     *				 转移时状态将被设置为0。
     *     			(此处对该值的使用与字段的其他用途无关,但简化了机制。)
     *   PROPAGATE:  应将releaseShared操作传播到其他节点。
 	 *				 这在doReleaseShared中设置(仅适用于头节点),以确保传播继续进行,
 	 *      		 即使其他操作已经介入。
     *   0:          默认状态
     *
     * 这些值按照数字顺序排列,以简化使用。
     * 非负值表示节点不会发出信号。
     * 因此,大多数代码不需要检查特定的值,只需检查符号即可。
	 * 
  	 * 该字段对于普通的同步节点初始化为0,
	 * 并且对于条件节点初始化为CONDITION,
     * 它使用CAS进行修改(或在可能的情况下,进行无条件的volatile写操作)。
     */
    volatile int waitStatus;
}

了解waitStatus的取值,再来看shouldParkAfterFailedAcquire方法就会轻松很多。

//AbstractQueuedSynchronizer
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 表明前驱节点已经设置为SIGNAL,前驱节点会负责唤醒当前节点的
        return true;
    if (ws > 0) {
        // 如果前驱节点的状态为CANCELLED,则向前寻找非CANCELLED状态的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus只会是0或PROPAGATE状态(为啥不会是condition呢?)  
         * 表明我们需要一个信号量,但不需要阻塞(通过park方法)
         * 调用者需要重试去确保该节点阻塞前不会被获取到。
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

只有前驱节点的waitStatus为SIGNAL,才继续执行下一步,否则进入下一轮循环。下一步就是执行parkAndCheckInterrupt方法

parkAndCheckInterrupt
//AbstractQueuedSynchronizer
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

通过LockSupport阻塞当前线程,返回判断当前线程是否被打断过。到此,循环内部的逻辑就执行完成了。如果在执行过程中出现异常会执行一个finally代码块中的内容。

cancelAcquire
//AbstractQueuedSynchronizer
private void cancelAcquire(Node node) {
    // 节点不存在就直接返回
    if (node == null)
        return;

    node.thread = null;

    // 跳过CANCELLED状态的前驱节点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

	// predNext是当前节点的非CANCELLED状态的前驱节点的下一个节点的引用
    Node predNext = pred.next;

    //将当前节点状态置为CANCELLED,这样就不会影响到其它线程。
    node.waitStatus = Node.CANCELLED;

    // 如果当前节点在尾部,将tail指向前驱节点,并将前驱节点的后继节点置为null
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // 当前节点如果status是SIGNAL,则它负责唤醒下一个节点,但是由于发生了异常,
        // 这项任务就需要交给非CANCELLED前驱节点
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
        //predNext是已经找好的引用,如果当前节点不是head的下一个节点,
        //让predNext指向当前节点的下一个节点即可。
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            //如果当前节点正好是head的下一个,自己状态已经变成CANCELLED了,
            //直接唤醒后继节点即可。
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}
//AbstractQueuedSynchronizer
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //  找到为非空且状态非CANCELLED的后继节点,唤醒该节点。
    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);
}

该节点在尝试获取锁的过程中如果出现了问题,就会执行cancelAcquire方法。队列中的每一个节点有一个任务就是负责唤醒后继节点。如果当前节点出现了问题,就需要找到前驱节点,让它替自己唤醒后继节点。如果不巧当前节点就是第一个节点(head后面),且后继节点有需要被唤醒的,就在自己CANCELLED之前“帮一下”。如此以来,acquireQueued也算分析完成了,到这里可能忘记从哪里出发了,我们回顾一下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

进行if语句内部,就开始执行selfInterrupt方法了。

selfInterrupt
static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

打断当前线程。注意:打断当前线程,当前线程并不会立刻停止运行,只是设置一个被打断的标志,线程可以通过该标志判断自己是否被打断过,如果被打断后,则可以执行被打断的逻辑。

释放流程

public void unlock() {
    sync.release(1);
}

ReetrantLock释放锁的逻辑就是直接调用unlock方法

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        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;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

可以看到释放锁的逻辑是很简单的。首先会判断当前线程和锁的拥有者是否是同一个,成功后唤醒下一个。

总结

尝试加锁,加锁成功就返回,失败就入队。执行完成后要唤醒队首节点。
可打断原理
不可打断模式
我们首先来看一下不可打断模式,这也是ReentrantLock的默认模式。核心在于我们分析的selfInterrupt。

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

该方法表面上看是要打断当前线程,但其实不然,在Java中打断当前线程,当前线程并不会立刻停止运行,只是设置一个被打断的标志,线程可以通过该标志判断自己是否被打断过,如果被打断后,则可以执行被打断的逻辑。而如果想打断当前线程,可以使用lockInterruptibly方法。

//ReentrantLock
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
//AbstractQueuedSynchronizer
public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}
//AbstractQueuedSynchronizer
private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

可以看出这三个方法与前面方法执行逻辑类似,有区别的地方在于会检查线程的打断标记来决定是否抛异常。如果线程被打断过,就抛出异常,打断线程。这里再次证明了,在Java中打断线程,线程会通过打断标记来决定是否执行被打断的逻辑。

公平锁实现原理

非公平锁的实现

//NonfairSync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {//line 5
        if (compareAndSetState(0, acquires)) {//line 6
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

line 5、line 6中可以看到:在没有线程抢到锁的情况下,所有线程都还会通过CAS竞争锁,这里就表明这是非公平锁。

公平锁的实现

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {//line 4
        if (!hasQueuedPredecessors() &&//line 5
            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;
}

line 4、line 5可以看出,非公平锁的时间逻辑在于会判断队列中是否由其它线程,如果由其它线程则加锁失败,这样就可以按照FIFO的方式来获取锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值