面试题:ReentrantLock 实现原理
ReentrantLock 实现原理
面试中遇到“ ReentrantLock 实现原理?”这个问题,我们应该怎么回答?是否直接就开始介绍 AQS, CLH 队列,这些高大上的词语。这样的回答会给面试官两个不好的印象:
- 问题回答没有逻辑,没有解释 ReentrantLock 与 AQS 等关系。
- 有死记硬背没有自己理解之嫌。
因此在这里提供一个回答问题的思路:按照功能介绍,需求分析,需求实现,源码实现,实现总结,共5步进行回答。这样有什么好处呢?
- 回答具有逻辑。按照使用到原理顺序回答问题,符合人的思维逻辑。
- 有自己的思考。在需求实现一节里详细介绍了如果自己来实现这些功能,会是怎么样的。
- 易于记忆。先是介绍怎么使用(功能介绍),然后是列举 ReentrantLock 类的方法和这些方法具有什么性质(需求分析),接下来是实现这些方法和功能(需求实现),最后是 JDK 怎么实现这些方法和功能的(源码实现)。
1.功能简介
1.1 ReentrantLock 是什么?
ReentrantLock 是一个可重入互斥锁,是 Lock 接口的实现类,是控制线程之间同步访问共享资源的一种方式。
1.2 为什么要使用 ReentrantLock?
ReentrantLock 用来保证线程安全。线程安全是在多线程并发访问共享资源时,通过同步机制协调各线程执行,来确保得到正确结果。
1.3 什么时候使用 ReentrantLock?
在需要控制线程之间同步访问共享资源时使用 Lock,但是只有以上需求时,我们完全可以使用 synchronized,没有必要使用 ReentrantLock。因此,当还有下面需求时才考虑使用 ReentrantLock :
- 阻塞可中断;当阻塞等待获取锁时,线程中断,可以中断阻塞。
- 公平锁;公平锁是指按照线程访问锁的先后顺序获取锁,synchronized 是非公平锁,所以要使用公平锁只能使用 ReentrantLock。
- 选择性通知(唤醒);多线程有等待/通知(wait,notify/notifyAll)功能,但是“通知“只能随机唤醒线程,而使用 ReentrantLock Condition 可以唤醒指定线程。
1.4 怎么使用ReentrantLock?
- 1.获取锁释放锁例子
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }}
- 2.阻塞可中断例子
Lock l = ...; try { l.lockInterruptibly(); try { // access the resource protected by this lock } finally { l.unlock(); }} } catch (InterruptedException e){ // 中断后处理 }
- 3.公平锁例子
Lock l = new ReentransLock(true);
- 4.选择性通知
假设我们有一个支持put和take方法的有界缓冲区。如果尝试在空缓冲区上执行take,那么线程将阻塞,直到某个项可用为止;如果试图在已满的缓冲区上执行put,那么线程将阻塞,直到有可用的空间为止。我们希望在单独的等待集中保持等待put线程和take线程,这样我们就可以使用优化,即当缓冲区中的项或空间变为可用时,一次只通知一个线程。这可以使用两个Condition实例来实现。public class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }
2.需求分析
我们根据 ReentrantLock 定义的方法,将这些方法具有的特性进行分析,并拆分为每个小的需求点,通过实现小的需求点,并组合,最终实现 ReentrantLock 的功能。这样做可以降低实现难度,也可以简化实现原理。
- 构造函数
- 公平锁,线程按照申请锁的顺序获取锁,先进先出。
- 非公平锁,不是申请锁的顺序获取锁,可能最后申请锁反而先获取锁。
- lock()
- 有阻塞获取锁,当能获取到锁时,方法返回;当不能获取到锁时,线程等待(阻塞)直到获取到锁。它功能可以分解为:无阻塞获取锁+阻塞。
- 可重入,获取到锁的线程可以多次获取到锁。
- lockInterruptibly()
- 有阻塞获取锁。
- 可重入。
- 阻塞可中断,线程等待(阻塞)获取锁期间,可以中断等待。
- tryLock()
- 无阻塞获取锁,当不能获取到锁时,方法返回false,没有阻塞的状态。
- 可重入。
- tryLock(long, TimeUnit)
- 有阻塞(一段时间)获取锁,当能获取到锁时,方法返回;当不能获取到锁时,线程等待(阻塞)一段时间,当过了这个时间还没能获取到锁,将中断阻塞。
- 可重入。
- 阻塞可中断。
- unlock()
- 释放锁。
- 解除阻塞,通知其他线程解除阻塞,尝试获取锁。
- newCondition()
3.需求实现
根据需求分析,探讨实现每个需求的原理。
-
公平锁
使用线程安全队列按照线程执行 lock 方法的先后顺序添加进队列。 -
非公平锁
随机让阻塞线程获取锁。 -
无阻塞获取锁
使用int state
变量作为锁状态的标志,0表示没有线程占用锁,1表示有线程占用锁。获取锁的伪码如下:private int state; public boolean tryLock(){ if state == 0 state = 1; return true; else return false; }
这样会有两个问题:
- 1.判断
state==0 and state=1
是非原子操作(执行两步)会有线程安全问题。 - 2.state 会出现一个线程修改时,另一个线程无法获取到最新值的情况,即存在可见性问题。
解决办法:
- 1.
unsafe.compareAndSwapInt(this, stateOffset, 0, 1)
,CAS 保证原子性。 - 2.
private volatile int state
,volatile 保证可见性。
所以最终伪码:
private volatile int state; public boolean tryLock(){ return unsafe.compareAndSwapInt(this, stateOffset, 0, 1); }
- 1.判断
-
阻塞
有5种方法实现阻塞:while(time>0)
这种方法占用 CPU 资源,不推荐。Object.wait(timeout)
需配合 synchronized 使用,不推荐。Thread.join
实现较为复杂,不推荐。Thread.sleep
实现较为复杂,不推荐。UNSAFE.park(false, 0)
LockSupport.park()
推荐。
-
可重入
使用Thread exclusiveOwnerThread
变量存当前占用锁的线程,当线程再次获取锁时,将state++
。伪码如下:private volatile int state; private transient Thread exclusiveOwnerThread; if (current == exclusiveOwnerThread) { state ++; return true; }
-
阻塞指定时间
在实现“互斥---一直阻塞”时,已经列举5种方法,并且推荐UNSAFE.park(false, 0)
LockSupport.park()
。所以为保持实现方法统一,推荐使用:UNSAFE.park(false, nanos)
LockSupport.parkNanos(blocker, nanos)
LockSupport.parkUntil(blocker, deadline)
-
解锁
只有持有锁的线程才能释放锁,当释放锁时state--
,伪码如下:private volatile int state; public void unlock() { if (current == exclusiveOwnerThread) { state --; } }
-
解除阻塞
解除阻塞和实现阻塞方法对应,方法如下:UNSAFE.unpark(thread)
LockSupport.unpark(thread)
-
响应中断
线程中断时调用Thread.interrupt()
,LockSupport.park()
会响应中断,解除阻塞。 -
newCondition
先不考虑,比较复杂,需单独写一篇文章。
4.源码实现
通过阅读源码,分析上面列出的需求,来分析源码是怎么实现需求的。
-
公平锁
CLH,线程安全的双向链表。 -
非公平锁
非公平锁执行 lock 方法时立刻尝试获取锁,而不是像公平锁一样进行排队。static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
-
无阻塞获取锁
CAS方式获取锁,unsafe.compareAndSwapInt(this, stateOffset, expect, update)
-
阻塞
LockSupport.park()
-
可重入
获取锁的持有线程,与当前线程比较,判断是否相等,相等即可获取锁,并且修改state
。state=0
表示没有线程获取锁;state=num
表示线程获取锁的次数为num,释放锁时需要释放num次。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; }
-
阻塞指定时间
LockSupport.parkNanos(this, nanosTimeout)
-
解锁
解锁需要做如下步骤:state -= releases
;setExclusiveOwnerThread(null)
。
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
-
解除阻塞
按照 CHL 队列顺序,通知下一个线程解除阻塞LockSupport.unpark(s.thread);
。 -
响应中断
源码实现与需求实现一致。线程中断时调用Thread.interrupt()
,LockSupport.park()
会响应中断,解除阻塞。 -
newCondition
先不考虑,比较复杂,需单独写一篇文章。
5.实现总结
本文按照,功能介绍,需求分析,需求实现,源码实现顺序讲解,介绍了 ReentrantLock 的使用、功能、实现。本文写了很多内容,如果可以用一句话介绍实现原理:
ReentrantLock 是使用链表、park、CAS、volatile 等方法实现的一个互斥且可重入的锁。
建议面试时遇到xxx原理一类的问题按照使用-原理来回答问题,这样我们可以给面试官留下不仅会使用而且还知道原理的印象。
posted on 2023-07-20 12:26 SmilingEye 阅读(125) 评论(0) 编辑 收藏 举报