ReentrantLock实现原理及源码分析

AQS

AQS全称是AbstractQueuedSynchronizer,它是java.util.concurrent包下的抽象类,是并发工具包的基础类,我们使用的很多JUC包下的工具类,例如ReentrantLock,CountDownLatch,CyclicBarrier、Semaphore等都是对AQS的实现,利用AQS我们可以简单高效的构建出同步器。
本文通过对最常用的可重入锁ReentrantLock源码进行分析,梳理其实现原理。本文不涉及Condition相关内容,这部分以后会单独成文。

主要结构

首先,我们看一下AbstractQueuedSynchronizer抽象类的结构

public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {

    /**
     * 队列的头节点,即持有锁的线程
     */
    private transient volatile Node head;

    /**
     * 队列的节点,新加入的节点放在队尾,形成队列
     */
    private transient volatile Node tail;

    /**
     * 同步锁的状态,0表示没有线程持有锁,大于0表示有线程持有锁,对于可重入锁
     * 这个值会大于1
     */
    private volatile int state;

	......
    
     /**
     * ......
     *
     * <pre>
     *      +------+  prev +-----+       +-----+
     * head |      | <---- |     | <---- |     |  tail
     *      +------+       +-----+       +-----+
     * </pre>
     * 
     * 等待队列采用的是CLH队列的一种变体,它是一个虚拟的双向队列,队列中每个节点封装请求共享资源的线程
     * 同时保存了当前节点在队列中的状态,前驱及后继节点,如上图所示,但需要注意的是,
     * head节点表示当前持有共享资源的线程,可以认为阻塞队列不包含head节点
     * ......
     */
    static final class Node {
        /** 标识节点在共享模式下 */
        static final Node SHARED = new Node();
        /** 标识节点在独占模式下 */
        static final Node EXCLUSIVE = null;

        /** 标识线程取消争抢锁 */
        static final int CANCELLED =  1;
        /** 标识本节点的后继节点可以被唤醒 */
        static final int SIGNAL    = -1;
        /** 本节点在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;

        /**
        * 链接condition队列或者用来表示共享模式
        */
        Node nextWaiter;

        ......
    }
}

OK看完AQS大体结构,再来看它的实现类ReentrantLock,在内部维护了一个Sync对象,Sync继承了AQS抽象类,并提供了两种实现方式,分别应用于公平锁和非公平锁。

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {

        /** 
         * lock方法由子类实现,公平锁和非公平锁竞争共享资源的方式不一样
         */
        abstract void lock();

        
        final boolean nonfairTryAcquire(int acquires) {
            ......
        }

        protected final boolean tryRelease(int releases) {
            ......
        }

    }

    /**
	* Sync object for non-fair locks
	*/
    static final class NonfairSync extends Sync {
        
        final void lock() {
             if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

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

    /**
	* Sync object for fair locks
	*/
    static final class FairSync extends Sync {
        
        final void lock() {
            acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
             ......
        }
       
    }

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

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

上面的代码比较多,可能看起来很乱,我们在这里梳理一下,

  1. 在ReentrantLock中维护了一个同步器Sync对象,它有公平锁和非公平锁两种实现方式,取决于ReentrantLock初始化时构造函数传的值是true还是false,默认是非公平锁。所以nonfairTryAcquire方法写在了Sync类中
  2. 查看AbstractQueuedSynchronizer源码知道提供了两种资源竞争方式,即独占和共享,分别对应两组方法tryAcquire+tryRelease或者tryAcquireShared+tryReleaseShared,由于ReentrantLock在设计上是独占锁,所以只对tryAcquire+tryRelease进行了实现
  3. tryAcquire的语义是尝试获取锁,无法获取则加入阻塞队列中。对于ReentrantLock而言,公平锁和非公平锁尝试获取锁的方式不一样,非公平锁会直接竞争锁,而公平锁会判断当前的独占线程是否为自己(这个后面会展开)。总结一下:tryAcquire由同步器自己实现具体竞争锁的逻辑,方法的结果决定了线程是否会加入等待队列。

线程抢锁

调用ReentrantLock.lock方法的线程,如果持有锁可以继续执行,反正则会阻塞等待锁释放。下面我们看一下线程竞争锁的过程。

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

	/** 
     * 父类AQS的方法,tryAcquire先尝试获取一下锁,如果返回true则获取成功了,直接结束
     * 否则,将线程加入阻塞队列,等待锁释放
     * addWaiter方法是创建一个等待队列的节点,将节点加入队列中
     * acquireQueued是将线程挂起等待唤醒
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    /**
    * 尝试获取锁,返回值表示是否获取到锁
    * 返回false表示有其他线程持有锁,返回true表示获取锁成功
    */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        // c == 0 当前没有线程持有锁
        if (c == 0) {
            //由于是公平锁,则需要判断队列中是否有线程在等待,
            //如果没有,通过CAS抢锁,抢锁成功则设置自己为当前锁的独占线程
            //如果CAS不成功,则表示锁被别的线程抢了
            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;
        }
        //没有获取到锁,回到acquire方法,将执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        return false;
    }
}


// if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
通过上面acquire方法可以看到,如果tryAcquire返回false表示尝试获取锁失败,会执行addWaiter和acquireQueued方法

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

/**
* 将线程包装成node节点并加入队列的队尾
*/
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)) {
            //双向连接,入队成功,新节点变成了尾巴,然后return
            pred.next = node;
            return node;
        }
    }
	//执行到这里,说明tail=null(队列为空)或者CAS失败了(线程竞争失败)
    enq(node);
    return node;
}

/**
* 自旋方式入队,如果队列为空或者线程竞争入队失败,则一直循环加入队列
* 自旋在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,我就多次竞争,总会排到的
*/
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        //队列为空的情况,CAS设置tail = head = new Node() 这时head是一个虚拟节点
        //这里没有return,执行完后继续for循环,下次就到else分支了
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //节点入队,CAS设置tail,如果CAS失败则继续循环,直到入队成功
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

/**
* 从addWaiter方法返回后,说明节点入队成功,执行acquireQueued方法
* 这个方法包含对线程进行挂起,以及唤醒后处理(线程排队以及锁的处理)
* 这个方法非常重要,下面梳理一下主要流程:
* 节点入队成功后,尝试获取锁,如果获取成功就不挂起了,线程继续运行
* 如果获取锁失败,判断是否需要挂起线程,判断依据是前驱节点waitStatus是否为-1
* 如果前驱节点是-1,则调用LockSupport.park挂起线程
*/
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //这个循环在什么时候结束? 线程获取到锁的时候
            for (;;) {
                final Node p = node.predecessor();
                // p == head 说明当前节点是阻塞队列的第一个节点(阻塞队列不包含head)
                // head节点可能是队列刚初始化的一个虚拟节点,也有可能是持有锁的节点
                // 所以,可以尝试去获取一下锁,如果获取成功,则把当前节点设置为head(持有锁),同时前驱的节点出队(释放锁)
                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;
        if (ws == Node.SIGNAL)
            //前驱节点是正常等待状态,则当前节点需要被挂起
            return true;
        if (ws > 0) {
        	//判断前驱节点的状态,如果前驱节点取消了排队,则一直向前寻找,直到找到还在排队中的节点
            //因为挂起线程的唤醒是依赖前驱节点的,所以当前节点的前驱节点一定要是"活"的
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //到这里说明前驱节点的状态是0,-2,-3(新节点入队时状态都是0)
            //将前驱节点的状态设置为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
    	//结合上面的代码知道返回false会再次进入循环,
    	//下一次循环就会走第一个if分支返回true
        return false;
}

/**
 * 在这里挂起线程,等待被唤醒
 */
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

执行到这里,线程加锁的操作完成,对于ReentrantLock而言,只有头节点的线程持有锁,队列中的线程处于阻塞状态,直到被LockSupport.unpark唤醒。

线程解锁

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

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;
	//如果是锁重入,要全部释放了才会返回true
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

/**
 *  线程解锁的方法,此时的node是头节点head
 */
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * 找到后继节点,并唤醒它
     * 后继节点可能是取消了排队,从队尾往前找,循环结束得到的是排在队列最前面的waitStatus <= 0节点
     * LockSupport.unpark唤醒此节点
     */
    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);
}


线程被唤醒后,从LockSupport.park继续执行,进入acquireQueued下一个循环,此时node的前驱是head,再次进入抢锁的逻辑了
// private final boolean parkAndCheckInterrupt() {
// LockSupport.park(this);
// return Thread.interrupted();
// }

总结:

加锁
  • tryAcquire-线程尝试获取锁,如果不存在锁竞争,就直接返回,它没有操作阻塞队列
  • addWaiter-将线程包装成node节点并CAS加入阻塞队列队尾
  • acquireQueued-自旋的方式获取锁,如果不能获取,则设置前驱节点为waitStatus=-1,表示等待被唤醒,然后线程挂起
  • waitStatus=-1的意思是代表后继节点需要被唤醒,为什么这么说呢?我们通过addWaiter方法知道新加入队尾的节点waitStatus都是0,之后acquireQueued方法会将前驱节点的状态更新为-1(SIGNAL),而自己的状态还是0,也就是说waitStatus=-1是后继节点更新的,通过这个状态知道后继节点是否等待被唤醒。
解锁
  • tryRelease-尝试释放锁,它同样没有操作阻塞队列
  • unparkSuccessor-当前节点的线程释放锁了,唤醒它的后继节点来抢锁

公平锁与非公平锁

ReentrantLock内部提供了公平锁和非公平锁两种实现,这里再扩展一下两者的区别,我们注意看他们的tryAcquire方法的区别

/**
* 公平锁
*/
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }
    
    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;
    }
}

/**
* 非公平锁
*/
static final class NonfairSync extends Sync {

    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    
    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) {
            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;
    }

}

可以看到,公平锁和非公平锁的区别在于
1.非公平锁在lock方法中一上来就会进行一次抢锁
2.tryAcquire方法中,公平锁多了一个!hasQueuedPredecessors()判断,它的语义是:
在线程尝试获取锁时,如果刚好当前没有线程持有锁,公平锁会去判断队列中还有没有在排队的线程(先来后到的原则),但非公平锁会直接尝试抢锁。
以上就是非公平锁和公平锁的区别,非公平锁在入队前进行两次抢锁,如果两次都没有抢到,就跟公平锁一样,进入队列中乖乖排队,非公平锁由于不需要保证时间顺序性,显然性能更好,但是可能会出现线程饥饿的情况。

文中很多内容参考了以下两位大佬的文章,并在此基础上增加了一些个人的思考,原文质量非常高,如果有没讲清楚的地方不妨移步
https://www.javadoop.com/post/AbstractQueuedSynchronizer
https://blog.csdn.net/hancoder/article/details/120954315

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值