共计 4447 个字符,预计需要花费 12 分钟才能阅读完成。
前言
上一次咱们曾经讲了 AQS
,如果对其不相熟的话倡议先去看看其实现原理,看完再来看ReentrantLock
就很简略了。
啃碎 JDK 源码 (一):String
啃碎 JDK 源码 (二):Integer
啃碎 JDK 源码 (三):ArrayList
啃碎 JDK 源码 (四):HashMap
啃碎 JDK 源码 (五):ConcurrentHashMap
啃碎 JDK 源码 (六):LinkedList
啃碎 JDK 源码(七):AbstractQueuedSynchronizer(AQS)
像 ReentrantLock
和 Synchornized
在面试中常常被用来比拟,如果想理解 Synchronized
的话能够看我另外一篇文章: 死磕 Synchronized
注释
先来理解一下一些外围属性:
public class ReentrantLock implements Lock, java.io.Serializable { | |
// 实现 AQS 的外部类 | |
private final Sync sync; | |
...... | |
} |
没错,ReentrantLock
没有什么值得注意的属性,因为曾经在 AQS
中定义好了,咱们只须要继承它而后进行简略的实现即可。
先看下 ReentrantLock
的用法:
public static void main(String[] args) {Lock lock = new ReentrantLock(); | |
lock.lock(); | |
try { | |
// 执行业务 | |
Thread.sleep(1000); | |
} catch (Exception e) {e.printStackTrace(); | |
} finally {lock.unlock(); | |
} | |
} |
只有调用 lock
办法就能够进行加锁操作,示意接下来的这段代码曾经被以后线程锁住,其余线程须要执行时须要拿到这个锁能力执行,而以后线程在执行完之后要显式的调用 unlock
开释锁。
留神:看源码之前你必须要对 AQS
比拟相熟才行,能够参考我上一篇博客:
啃碎 JDK 源码(七):AbstractQueuedSynchronizer(AQS)
咱们来跟进源码看一下,先来看咱们的加锁 lock
办法:
public void lock() {sync.lock(); | |
} | |
// Sync 继承了 AQS | |
abstract static class Sync extends AbstractQueuedSynchronizer {abstract void lock(); | |
...... | |
} |
能够看到是调用外部类的 lock
办法,而它是一个形象办法,咱们看下谁继承了这个形象接口:
FairSync
和 NonfairSync
是 ReentrantLock
的另外两个外部类。顾名思义一个是偏心锁,一个是非偏心锁。(偏心锁就是永远都是队列的第一位能力失去锁)
在 AQS
有一个 同步队列 (CLH
),是一种 先进先出队列 。偏心锁的意思就是 严格依照这个队列的程序来获取锁,非偏心锁的意思就是不肯定依照这个队列的程序来。
在 new 对象的时候便会对 sync 初始化,如下:
public ReentrantLock() {sync = new NonfairSync(); | |
} | |
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();} |
能够看出默认是非偏心锁,如果传 true 则初始化为偏心锁。
那咱们首先来看看非偏心锁:
static final class NonfairSync extends Sync { | |
private static final long serialVersionUID = 7316153563782823691L; | |
final void lock() { | |
// CAS 批改状态 | |
if (compareAndSetState(0, 1)) | |
// 设置独占线程 | |
setExclusiveOwnerThread(Thread.currentThread()); | |
else | |
// 进入队列期待 | |
acquire(1); | |
} | |
// tryAcquire 是 AQS 的形象办法,咱们这里对其实现 | |
protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires); | |
} | |
} |
首先用 compareAndSetState
办法应用 CAS 批改 state 状态变量的值,如果批改胜利的话应用 setExclusiveOwnerThread(Thread.currentThread())
办法将以后线程设置为独占锁的持有线程,否则调用 AQS 的 acquire
办法进去队列期待解决。
接下来看一下 acquire
办法:
public final void acquire(int arg) {if (!tryAcquire(arg) && | |
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) | |
selfInterrupt();} |
该办法是 AQS 里的办法,咱们上次曾经介绍过了,这里间接截过去看下:
这次咱们次要关注由子类 ReentrantLock
实现的 tryAcquire
办法:
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"); | |
// 间接批改 state 遍历,因为曾经持有锁,不须要用 CAS 去批改 | |
setState(nextc); | |
return true; | |
} | |
return false; | |
} |
下面代码和咱们在上次手动实现一个可重入锁的代码差不多,这里就不再开展。
那接下来看一下 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; | |
} |
release
办法在 AQS 类中定义好了,咱们子类次要实现 tryRelease
办法:
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; | |
} |
这段代码上篇文章咱们也曾经讲过了,如果遗记的同学能够回头看看。
看完非偏心锁的最初来看看偏心锁的加锁办法:
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; | |
} | |
} |
其实代码根本和后面一样,只是多了 hasQueuedPredecessors
办法用来判断是否存在比期待更久的线程,因为要依照等待时间程序获取资源,其它的这里就不再细说了。
其它疑难
以下问题来自 从源码角度了解 ReentrantLock
为什么基于 FIFO 的同步队列能够实现非偏心锁?
由 FIFO 队列的个性知,先退出同步队列期待的线程会比后退出的线程更凑近队列的头部,那么它将比后者更早的被唤醒,它也就能更早的失去锁。从这个意义上,对于在同步队列中期待的线程而言,它们取得锁的程序和退出同步队列的程序统一,这显然是一种偏心模式。然而,线程并非只有在退出队列后才有机会取得锁,哪怕同步队列中已有线程在期待,非偏心锁的不偏心之处就在于此。回看下非偏心锁的加锁流程,线程在进入同步队列期待之前有两次抢占锁的机会:
- 第一次是应用
compareAndSetState
办法尝试批改 state 变量,只有在以后锁未被任何线程占有 (包含本身) 时能力胜利。 - 第二次是在进入同步队列前应用
tryAcquire(arg)
尝试获取锁。
只有这两次获取锁都失败后,线程才会结构结点并退出同步队列期待。而线程开释锁时是先开释锁 (批改 state 值),而后才唤醒后继结点的线程的。试想下这种状况,线程 A 曾经开释锁,但还没来得及唤醒后继线程 C,而这时另一个线程 B 刚好尝试获取锁,此时锁恰好不被任何线程持有,它将胜利获取锁而不必退出队列期待。线程 C 被唤醒尝试获取锁,而此时锁曾经被线程 B 抢占,故而其获取失败并持续在队列中期待。
那咱们在开发中为什么大多应用非偏心锁?很简略,因为它性能好啊。
为什么非偏心锁性能好
- 线程不用退出期待队列就能够取得锁,不仅 免去了结构结点并退出队列的繁琐操作,同时也节俭了线程阻塞唤醒的开销,线程阻塞和唤醒波及到线程上下文的切换和操作系统的零碎调用,是十分耗时的。。
- 缩小 CAS 竞争。如果线程必须要退出阻塞队列能力获取锁, 那入队时 CAS 竞争将变得异样强烈,CAS 操作尽管不会导致失败线程挂起,但一直失败重试导致的对 CPU 的节约也不能漠视。
总结
无关 ReentrantLock
的常识就介绍到这里了,有什么不对的中央请多多指教。