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