Java多线程系列之JUC锁 - ReentrantLock

1、ReentrantLock的介绍

    ReentrantLock在源码中的解释是做为可重入互斥锁与synchronized有基本相同的行为和语义可是它在此基础上又扩展了其余的一些功能。ReentrantLock锁的线程持有者是上一个成功加锁且不曾释放的线程。ReentrantLock 是独占锁在同一时间只能被一个线程持有,它做为可重入锁表如今它可被单个线程屡次持有,源码注释中提到当一个线程调用lock方法的时候,若是当前持有锁的是该线程自己,将当即成功获取锁,能够经过isHeldByCurrentThread或getHoldCount确认。java

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

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

2、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分别对应非公平锁和公平锁。设计模式

3、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)app

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的非公平锁也支持单线程重入。ui

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

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)唤醒队列里的其余线程(当前节点第一个正常的后继节点线程)

相关文章
相关标签/搜索