头图

独占锁 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 阻塞队列中然后被挂起。

阻塞队列线程2,线程3

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

由于线程 1 释放了锁,所以阻塞队列中的线程 2,线程 3 都会有机会获取到该锁,假如使用的是公平锁,那么线程 2 则会获取到该锁,然后从 AQS 阻塞队列中移除线程 2 对应的 Node 节点。

阻塞队列线程3


神秘杰克
765 声望382 粉丝

Be a good developer.