独占锁 ReentrantLock 原理

介绍

ReentrantLock 是可重入的独占锁,同时只能有一个线程能够获取该锁,其余获取该锁的线程会被阻塞而后放入到该锁的AQS阻塞队列中。

它具备与synchronized雷同的根本行为和语义,但 ReentrantLock 更灵便、更弱小,减少了轮询、超时、中断等高级性能,并且还反对偏心锁和非偏心锁

从类图能够看到,ReentrantLock 还是应用 AQS 来实现的,并且能够依据参数来抉择其外部是一个偏心锁还是非偏心锁,默认为非偏心锁

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

Sync 类间接继承 AQS 类,它的子类 NonfairSync 和 FairSync 别离实现了获取锁的非偏心和偏心策略。

static final class NonfairSync extends Sync {}static final class FairSync extends Sync {}

在 ReentrantLock 中 AQS 的 state 状态值示意线程获取锁的可重入次数,在默认状况下,state 值为 0 示意以后所没有任何线程持有。当一个线程第一次获取该锁后,会尝试应用 CAS 将 state 设置为 1,如果 CAS 胜利则以后线程获取该锁,而后记录该锁的持有者为以后线程。在该线程第二次获取该锁后,则会将 state 设置为 2,这就是可重入次数。在该线程开释锁时,会尝试应用 CAS 将 state 减 1,如果减 1 后为 0 则开释锁。

获取锁

void lock()

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

当线程调用该办法时,如果以后锁没有被其余线程占用并且以后线程之前没获取过该锁,则以后线程获取,而后设置锁的拥有者为本人,随后设置 AQS 的 state 为 1,而后返回。

如果以后线程曾经获取过该锁,则只是简略的将 AQS 的 state 加 1,而后返回。

如果该锁曾经被其余线程所持有,则以后线程会进入 AQS 的阻塞队列中阻塞挂起。

在 ReentrantLock 中的 lock()办法委托给了 sync 类,sync 则依据 ReentrantLock 的构造函数抉择应用 NonfairSync 或 FairSync。

咱们先看看非偏心锁的实现。

final void lock() {    // CAS设置状态值    if (compareAndSetState(0, 1))        setExclusiveOwnerThread(Thread.currentThread());    else        // 调用AQS的acquire办法        acquire(1);}

在 lock()中首先调用了 compareAndSetState 办法,因为默认 state 状态值为 0,所以第一个线程在首次调用该办法时通过 CAS 会设置为 1,随后胜利获取到该锁,而后通过 setExclusiveOwnerThread 办法将锁持有者设置为以后线程。

当有其它线程通过 lock()来获取该锁时候,会 CAS 失败进入 else 调用 acquire(1)办法并且传参数为 1,上面咱们再看一下 acquire 办法。

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

之前文章讲过,AQS 没有提供 tryAcquire 办法的实现,咱们看一下 ReentrantLock 重写的 tryAcquire 办法,这里咱们还是看非偏心锁的实现。

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

该办法首先查看以后状态值是否为 0,为 0 则阐明锁目前是闲暇状态,而后尝试 CAS 获取锁,胜利后 state 设置为 1,而后锁持有者为以后线程。

如果 state 不为 0,则阐明锁曾经被持有,如果持有者正好是以后线程则进行 state 加 1,而后返回 true,须要留神,如果 nextc < 0 则阐明锁可能溢出。

如果以后线程不是持有者则返回 false 随后退出 AQS 阻塞队列。

上面咱们看一下偏心锁的实现。

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

偏心锁和非偏心锁的 tryAcquire 办法不同之处就是多了一个 hasQueuedPredecessors()办法,该办法就是实现偏心锁的外围代码。

public final boolean hasQueuedPredecessors() {    //读取头节点    Node t = tail;    //读取尾节点    Node h = head;    //s是首节点h的后继节点    Node s;    return h != t &&        ((s = h.next) == null || s.thread != Thread.currentThread());}

在该办法中,因为队列是 FIFO 的,所以须要判断队列中有没有相干线程的节点曾经在排队了。有则返回 true 示意线程须要排队,没有则返回 false 则示意线程无需排队。

首先咱们看第一个条件h != t

  • 头节点和尾节点都为 null,示意队列都还是空的,甚至都没实现初始化,那么天然返回 fasle,无需排队。
  • 头节点和尾节点不为 null 然而相等,阐明头节点和尾节点都指向一个元素,示意队列中只有一个节点,这时候也无需排队,因为队列中的第一个节点是不参加排队的,它持有着同步状态,那么第二个进来的节点就无需排队,因为它的前继节点就是头节点,所以第二个进来的节点就是第一个能失常获取同步状态的节点,第三个节点才须要排队,期待第二个节点开释同步状态。

接下来咱们看第二个条件,(s = h.next) == null,如果h != t && s == null则阐明有一个元素将要作为 AQS 的第一个节点入队,则返回 true。

接下来看第三个条件,s.thread != Thread.currentThread() ,判断后继节点是否为以后线程。

♀️ 举例,状况一:

h != t 返回true,(s = h.next) == null 返回false , s.thread != Thread.currentThread()返回false

首先 h != t 返回true,阐明队列中至多有两个不同节点存在;

(s = h.next) == null 返回false,阐明头节点之后是有后继节点存在;

s.thread != Thread.currentThread()返回 false,阐明以后线程和后继节点雷同;

阐明曾经轮到以后节点去尝试获取同步状态,无需排队,返回 false

♀️ 举例,状况二:

h != t 返回true,(s = h.next) == null 返回true

首先 h != t 返回true,阐明队列中至多有两个不同节点存在;

(s = h.next) == null 返回true,阐明头节点也就是哨兵节点之后没有后继节点;

返回 true,阐明须要排队

♀️ 举例,状况三:

h != t 返回true,(s = h.next) == null 返回false,s.thread != Thread.currentThread()返回true

首先 h != t 返回true,阐明队列中至多有两个不同节点存在;

(s = h.next) == null 返回false,阐明头节点之后是有后继节点存在;

s.thread != Thread.currentThread()返回true,阐明后继节点的线程不是以后线程,阐明后面曾经有人在排队了,还是得老老实实排队。

返回 true,阐明须要排队

void lockInterruptibly()

public void lockInterruptibly() throws InterruptedException {    sync.acquireInterruptibly(1);}public final void acquireInterruptibly(int arg)            throws InterruptedException {        // 如果以后线程被中断,则抛出异样        if (Thread.interrupted())            throw new InterruptedException();        //尝试获取资源        if (!tryAcquire(arg))            //调用AQS可被中断的办法            doAcquireInterruptibly(arg);}

该办法和 lock()办法相似,只不过它会对中断进行响应,就是以后线程在调用该办法时,如果他其余线程调用了以后线程的 interrupt()办法,则以后线程会抛出 InterruptedException 异样。

boolean tryLock()

public boolean tryLock() {    return sync.nonfairTryAcquire(1);}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;}

该办法尝试获取锁,如果以后锁没有被其余线程持有,则以后线程获取该锁并返回 true,否则返回 false。

留神:该办法不会引起以后线程阻塞。

boolean tryLock(long timeout,TimeUnit unit)

public boolean tryLock(long timeout, TimeUnit unit)        throws InterruptedException {    return sync.tryAcquireNanos(1, unit.toNanos(timeout));}

与 tryLock 不同之处在于,设置了超时工夫,如果超时工夫到还没有获取到该锁,则返回 false。

开释锁

void unlock()

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;    //如果不是锁持有者调用unlock则抛出异样    if (Thread.currentThread() != getExclusiveOwnerThread())        throw new IllegalMonitorStateException();    boolean free = false;    // 如果以后可重入次数为0 则状况锁持有线程    if (c == 0) {        free = true;        setExclusiveOwnerThread(null);    }    setState(c);    return free;}

该办法首先尝试开释锁,如果 tryRelease()办法返回 true 则开释该锁,否则只是减 1,如果不是锁持有者去调用 unlock 则抛出 IllegalMonitorStateException 异样。

代码实际

/** * @author 神秘杰克 * 公众号: Java菜鸟程序员 * @date 2022/1/21 * @Description 应用ReentrantLock实现简略的线程平安List */public class ReentrantLockList {    private List<String> array = new ArrayList<>();    private volatile ReentrantLock lock = new ReentrantLock();    public void add(String e) {        lock.lock();        try {            array.add(e);        } finally {            lock.unlock();        }    }    public void remove(String e) {        lock.lock();        try {            array.remove(e);        } finally {            lock.unlock();        }    }    public String get(int index) {        lock.lock();        try {            return array.get(index);        } finally {            lock.unlock();        }    }}

该类在通过操作 array 之前,通过加锁来保障同一时间只有一个线程能够操作 array,然而也只能有一个线程能够对 array 元素进行拜访。

总结

当同时有三个线程尝试获取独占锁 ReentrantLock 时,如果线程 1 获取到,则线程 2、3 都会被转换为 Node 节点随后被放入 ReentrantLock 对应的 AQS 阻塞队列中而后被挂起。

假如线程 1 在获取到锁之后,调用了锁创立的条件变量 1 进入 await 后,线程 1 就会开释该锁。而后线程 1 会被转换为 Node 节点插入条件变量 1 的条件队列中。

因为线程 1 开释了锁,所以阻塞队列中的线程 2,线程 3 都会有机会获取到该锁,如果应用的是偏心锁,那么线程 2 则会获取到该锁,而后从 AQS 阻塞队列中移除线程 2 对应的 Node 节点。