关于java:Java-JUC-ReentrantLock解析

31次阅读

共计 6495 个字符,预计需要花费 17 分钟才能阅读完成。

独占锁 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 节点。

正文完
 0