Java多线程系列之JUC锁 - ReentrantLock

一、ReentrantLock的介绍

    ReentrantLock在源码中的解释是作为可重入互斥锁与synchronized有基本相同的行为和语义但是它在此基础上又扩展了其他的一些功能。ReentrantLock锁的线程持有者是上一个成功加锁且未曾释放的线程。ReentrantLock 是独占锁在同一时间只能被一个线程持有,它作为可重入锁表现在它可被单个线程多次持有,源码注释中提到当一个线程调用lock方法的时候,如果当前持有锁的是该线程本身,将立即成功获取锁,可以通过isHeldByCurrentThread或getHoldCount确认。

    ReentrantLock分为公平锁和非公平锁,可通过在创建实例时在构造函数中指定可选是否公平参数fairness确定使用公平锁还是非公平锁,若未指定则默认使用非公平锁。公平锁与非公平锁的区别在于获取锁的顺序,公平锁保证等待时间最长的线程有限获取锁而非公平锁不保证线程访问获取锁的任何顺序,主要是通过一个FIFO等待队列实现的,若为公平锁则线程一次排队获取锁,每次由队列头优先获取锁,非公平锁模式下无论等待线程是否位于队列头部只要锁释放都有同等机会竞争锁。在多线程环境下使用公平锁的吞吐量可能会低于使用默认的非公平锁,但是公平锁可以保证了线程获取锁更小的时间差异,且有效防止了线程饥饿(某个线程每次CPU执行机会都被其他线程抢占导致饥饿致死)。注意公平锁仅仅保证获取锁的公平性但不保证线程的调度顺序,使用公平锁的多个线程中的某个线程可能获得比其他线程更多的成功执行机会,前提是其他线程未获取到该锁且其他活跃线程未被处理,这很容易理解我们在程序中公平锁只能保证最久等待的线程优先获取锁但是对于线程调度这是由操作系统控制的。无论是公平锁还是非公平锁,tryLock 方法并没有使用公平设置。即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。

    推荐在使用lock方法锁定的同步代码块中使用try..finally包裹在finally中使用unlock释放锁。ReentrantLock在反序列时一定是解锁状态无论它在之前的序列化时是否是加锁状态,此外虽说ReentrantLock对单一线程可重入,但是同一个线程最多2147483647的递归加锁限制,若超出这个限制会在加锁方法报错

二、ReentrantLock数据结构

public class ReentrantLock implements Lock, java.io.Serializable {
    // 同步器,Sync是AbstractQueuedSynchronizer的派生类,ReentrantLock实现锁主要通过该对象实现
    private final Sync sync;

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

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

}

    ReentrantLock实现了Lock类,Lock定义了相对synchronized更灵活且更多的锁操作,实现了Serializable支持序列化。构造方法包括默认构造方法和一个带参构造方法,默认构造方法创建的是非公平锁,带参构造方法ReentrantLock(boolean fair)基于fair创建公平锁或者非公平锁。底层也是基于AQS(AbstractQueuedSynchronizer)实现的,ReentranLock的核心是内部成员对象sync,所属类Sync是ReentrantLock内部定义的AQS派生类,在ReentranLock有两个实现类FairSync和FairSync分别对应非公平锁和公平锁。

三、ReenTrantLock源码解析

1 - void lock()方法 - 获取锁
    public void lock() {
        sync.lock();
    }

    我们先来看下ReentrantLock非公平锁该方法的实现,进入NonfairSync类lock方法

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

    逻辑较为简单,首先基于CAS算法调用compareAndSetState尝试获取锁,若锁未被持有即锁同步状态state=0那么它将尝试更新锁状态state=1表示锁已经被持有,这里锁的同步状态state还用于保存锁持有线程获取锁操作的次数,这里使用compareAndSet原子更新state的值是为了确保state在上一次检查之后该状态未发生变更,更新锁同步状态state成功之后调用setExclusiveOwnerThread方法设置锁持有线程exclusiveOwnerThread为当前线程,也是AQS内部成员变量,用于区分多线程获取锁操作时重入锁还是竞争锁。我们看下该方法源码:

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    就是更新成员变量exclusiveOwnerThread为当前线程对象,若获取锁失败则调用acquire方法尝试获取锁,进入该方法(方法源码位于AbstractQueuedSynchronizer)

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

    acquire方法首先通过tryAcquire方法尝试获取锁,若获取锁成功则直接返回否则会先通过addWaiter方法将当前线程加入CLH锁等待队列末尾然后调用acquireQueued方法,等待前面的线程执行并释放锁之后获取锁,若在休眠过程中被中断过则调用selfIntegerrupt方法自己产生一个中断。tryAcquire方法公平锁和非公平锁的实现不同,我们进入NofairSync类内部看下该方法的实现

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

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

    由NonfairSync的内部实现源码可知,非公平锁中线程尝试获取锁并没有用到CLH等待队列,相反只要锁空闲(state=0)就可以直接获取锁并且NonfairSync的非公平锁也支持单线程重入。

    若获取锁失败首先调用addWaiter方法,该方法是在AQS实现的,进入该方法源码看下做了什么

    private Node addWaiter(Node mode) {
        // 为当前线程新建一个Node节点,节点的模型是前面传过来的独占锁模型
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        // 若CLH队列不为空,则将当前线程节点添加到CLH队列末尾
        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;
                }
            }
        }
    }

    很简单addWaiter方法就是判断CLH等待队列是否为空,若为空则新建一个CLH表头;然后将当前线程节点添加到CLH末尾。否则,直接将当前线程节点添加到CLH等待队列末尾,添加线程到等待队列之后接下来看下acquireQueued方法,进入该方法源码

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            // interrupt标识在CLH等待队列的调度中当前线程在休眠时有没有被中断过
            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;
                }
                // 线程若应该阻塞则调用park阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                //放弃获取锁
                cancelAcquire(node);
        }
    }

    简单来说acquireQueued方法的作用是逐步的去执行CLH等待队列的线程,如果当前线程获取到了锁,则返回;否则,当前线程进行休眠,直到唤醒并重新获取锁了才返回

    下面顺便进入selfInterrupt()方法源码看下它作了啥

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

    看来就是调用当前线程对象的interrupt方法做了中断。

    总结ReentrantLock的非公平锁的获取锁方法lock的基本逻辑如下:

1)首先尝试获取锁,若锁处于空闲状态,获取锁成功,直接返回;

2)若锁已被占用且是当前线程,ReentrantLock支持锁重入,如果锁同步状态state(或者称之为当前锁持有线程递归加锁次数)超过int的最大值则抛出异常,否则加锁成功,更新state;

3)若锁已被占用且持有锁的不是当前线程则将当前线程加入CLH锁等待队列末尾,等待前面的线程执行并释放锁之后获取锁,若在休眠过程中被中断过则调用selfIntegerrupt方法自己产生一个中断。

    接下来 我们先来看下ReentrantLock公平锁该方法的实现,进入FairSync类lock方法

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

    内部直接调用AQS类的acquire方法,我们进入该方法

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

    看到没有与非公平锁一样都是调用acquire方法实现锁获取,不同点只在于tryAcquire方法的实现,这种设计模式也是开发中经常用到的模板方法设计模式,我们下面贴出FairSync的tryAcquire方法实现

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

    方法的大部分逻辑很简单,我们看下c = 0条件下调用的hasQueuedPredecessors方法内部作了什么,该方法是在AQS类定义的。

    public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

    hasQueuedPredecessors方法主要判断当前线程是否位于CLH等待队列的队首

    总结ReentrantLock公平锁的获取锁方法lock的基本实现逻辑如下:

1)获取AQS的同步状态state和当前线程对象

2)若锁处于空闲状态,判断当前CLH等待队列中是否存在等待线程,若不存在则获取锁成功,直接返回;

3)若锁处于空闲状态,CLH等待队列中存在等待获取锁线程则获取锁失败,将当前线程加入到CLH等待队列末尾,逐步的去执行CLH等待队列的线程,如果当前线程获取到了锁,则返回;否则,当前线程进行暂时休眠,直到唤醒并重新获取锁才返回;

4)若锁已被线程持有且持有线程的是当前线程,则更新同步状态(锁持有线程递归加锁次数)state判断是否超出最大次数限制,若未超出限制则加锁成功;

    总结ReentrantLock公平锁和非公平锁获取锁方法的差异性主要表现在获取锁的策略方面,多线程环境下线程在获取非公平锁的时候只判断锁处于空闲状态就可以获取,而公平锁需要基于CLH等待队列排队获取锁。

2 - void unlock()方法 - 释放锁
    public void unlock() {
        sync.release(1);
    }

    方法内部调用了sync.release(int)方法,该方法是在AQS(AbstractQueuedSynchronizer)类实现的,进入该方法

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    AQS的release方法首先调用了tryRelease方法获取锁,该方法的实现在ReentrantLock内部的Sync类中,进入该类的该方法

        protected final boolean tryRelease(int releases) {
            // 更新锁的递归加锁次数state
            int c = getState() - releases;
            // 锁持有锁线程不是当前线程抛出异常,只有锁持有线程才可以释放锁资源
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 若c=0还需要将线程持有者内部成员变量设置为null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 否则只是锁的递归释放,同一线程多次加锁需要多次释放
            setState(c);
            return free;
        }

    release方法基本流程总结如下:1)判断锁持有线程是否是当前线程若不是直接抛出异常;2)若锁的同步状态state = 0还需要将锁持有线程变量设置为null;3)若只是锁的递归释放,即当前线程多次加锁则需要释放,除了最后一次释放需要重置所持有线程为null之外只需要更新锁的同步状态(即state原子递减)。

    接下来我们分析下unparkSuccessor方法,该方法的作用是唤醒CLH队列的后继等待节点

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

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        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);
    }

    下面是线程状态waitStatus的说明

CANCELLED[1]  -- 当前线程已被取消
SIGNAL[-1]    -- “当前线程的后继线程需要被unpark(唤醒)”。一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被release或cancel掉,因此需要唤醒当前线程的后继线程。
CONDITION[-2] -- 当前线程(处在Condition休眠状态)在等待Condition唤醒
PROPAGATE[-3] -- (共享锁)其它线程获取到“共享锁”
[0]           -- 当前线程不属于上面的任何一种状态。

    通过分析源码可知unparkSuccessor方法的基本逻辑是:

1)首先更新释放CLH等待队列中的当前线程;

2)然后唤醒CLH等待队列当前线程后继节点中不为空或者未取消的线程,它首先判断等待队列中当前线程节点的后继节点是否是正常的,若是直接唤醒后继节点,否则从队列尾部往前递归回溯获取当前节点后继节点之后最近一个正常节点, 唤醒它。

    总结ReentrantLock释放锁不区分公平锁和非公平锁,它的主要流程是1)递减同步状态变量state,当state=0的时候释放锁将当前锁持有线程引用置为null;2)唤醒队列里的其他线程(当前节点第一个正常的后继节点线程)

转载于:https://my.oschina.net/zhangyq1991/blog/1941734

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值