Reentrant 可重入解释

做者:知乎用户
连接:https://www.zhihu.com/question/37168009/answer/88086943
来源:知乎
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

咱们来看看问题,按照如今我看到的状况,题干是:“怎样证实synchronized锁,Lock锁是可重入的”,外加一个Java的标签。html

Java中,Synchronized确实是可重入的。另外Lock锁这个定义并不许确,在Java中Lock只是一个接口,而且在doc中并无说明实现类必定是须要具有可重入的特性。Lock的实现众多,其中最多见也是最为任何Java程序员熟知的是ReentrantLock。可是注意,不必定Lock的子类就是可重入的,例如netty中就有一个比较有趣的NoReentrantLock的实现。java

那么下面内容就以题目是Synchronized和ReentrantLock为前提进行。程序员

咱们第一步要明确什么是“ 可重入的”。其对应的英文单词是: Reentrant,哦不对,其实准确的说应该是“ Re-entrant”。wikipedia有一个 Reentrancy(computing)的解释。不过在ReentrantLock的doc中找到这段话:
A ReentrantLock is owned by the thread last successfully locking, but not yet unlocking it. A thread invoking lock will return, successfully acquiring the lock, when the lock is not owned by another thread. The method will return immediately if the current thread already owns the lock.

最后一句话尤为重要,若是当前占用这个Reentrant的人就是当前线程,那么就会当即返回。换成大白话说就是,一个线程获取到锁以后能够无限次地进入该临界区 (经过调用lock.lock())。固然一样也须要等同次数的unlock操做(这句话是我加的数据结构

OK,既然咱们已经明白了Reentrant的含义。那么如何证实呢?写个程序是最简单的办法,一个线程递归的调用一个须要加锁的函数(不要递归太深),看会不会hog住线程。这都是很好很好的,可我恰恰不喜欢,引自《白马啸西风》。我仍是更倾向于learn java in the hardest way。框架

先,简单介绍一下普通的lock的实现原理,这里只介绍加锁部分,下面是伪码形式:
public void lock() { // step 1. try to change a atomic state boolean ok = state.compareAndSet(0, 1); // step 2. set exclusive thread if ok if (ok) { setExclusiveThread(Thread.current()); // 这只是个标志位,不用太介意 return; } // step 3. enqueue enqueue(); // step 4. block Unsafe.park(); // step 5. retry lock(); } 

小朋友们不要轻易模仿。没有谁用这种傻逼的递归写法的,除了我。完整的代码比这个复杂,除了基本的流程,还要处理是不是公平锁,处理线程中断,以及一系列的无锁数据结构等等。

几个要点:
  • 经过一个原子状态来控制谁进入临界区
  • 经过一个链表队列,记录等待获取锁的线程
  • 经过Unsafe的park()函数,来把当前线程的运行状态设置成挂起,而且中止调度
  • 当已经获取锁的线程调用unlock()函数的时候,就会使用Unsafe.unpark()函数来唤醒等待队列头部的线程
  • 唤醒以后,线程继续试着获取锁,失败则递归,成功则返回

慢着,知道上面的东西,离咱们证实题干还有必定的距离,继续看。
Tips: 整个concurrent包源自于JSR-166,其做者就是大名鼎鼎的 Doug Lea,说他是这个世界上对Java影响力最大的我的,一点也不为过。由于两次Java历史上的大变革,他都间接或直接的扮演了举足轻重的角色。一次是由JDK 1.1到JDK 1.2,JDK1.2很重要的一项新创举就是Collections,其Collections的概念能够说承袭自Doug Lea于1995年发布的第一个被普遍应用的collections;一次是2004年所推出的Tiger。Tiger广纳了15项JSRs(Java Specification Requests)的语法及标准,其中一项即是JSR-166
就是这个小朋友,概括总结出,嗯各类同步手段底层都须要一些共同的东西,因此写了一个类叫java.util.concurrent.locks.AbstractQueuedSynchronizer。后来被简称为AQS框架,该框架将加锁的步骤模板化了以后,提供了基本的列表、状态控制等等手段。咱们能够简单看看lock的过程他是如何抽象的:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } 
一共四步:
  1. tryAcquire,抽象方法,由子类实现,子类经过控制原子变量来表示是否获取锁成功,相似于上文代码的Step一、Step2
  2. addWaiter,已经实现的方法,表示将当前线程加入等待队列,相似于上文的Step3
  3. acquireQueued(),挂起线程,唤醒后重试,相似于上文的Step四、Step5
  4. 处理线程中断标志位。

咱们只须要记住一个重要的地方就是,子类只须要实现tryAcquire方法,就能够实现一个锁,嗯,不错!而这个tryAcquire方法最重要的就是利用AQS类中提供的原子操做来控制状态。咱们看一个最简单的Mutex的例子:
public boolean tryAcquire(int acquires) {
   assert acquires == 1; // Otherwise unused
   if (compareAndSetState(0, 1)) {
     setExclusiveOwnerThread(Thread.currentThread());
     return true;
   }
   return false;
 }

简单解释一下,compareAndSetState是父类AQS中提供的protected方法,setExclusiveOwnerThread同理。如此咱们就实现了一个简单的Mutex。函数

如今咱们考虑一个问题,这个基于AQS实现的Mutex是否是可重入的呢?固然不是,线程A调用lock方法,而后就调用到这个tryAcquire函数中,显然这个状态就是被设置成了1。线程A第二次进来的时候,再次控制这个原子变量,发现就很差使了,就进入等待队列。本身就被本身等死了。ui

好,最后就是重点,ReentrantLock也是在AQS的基础上实现的,那么咱们来看,他的tryAcquire方法是怎么写的。 简单起见,ReentrantLock有公平和非公平的两种实现,咱们只关注可重入的特色,这里就不介绍,咱们直接看非公平的版本。
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; } 
我来解释下这段代码:
  • 若是当前的state(AQS提供的原子变量)=0,意味着没有人占用,那么咱们compareAndSet来占用,而且设置本身为独占线程
  • 若是独占线程就是当前线程,那么说明就是我本身锁住啦(可重入),那么把state计数累加。

貌似这样就说通了。还有一个点就是不要小看这个累加哦,在unlock的时候也是一个累减的过程,也就是同一个线程针对同一个ReentrantLock对象调用了10次lock操做,那么对应的,就须要调用10次unlock操做。才会真正的释放lock。atom

我想差很少应该能够证实了吧..spa

对这个类比较感兴趣的小朋友能够参考爸爸的两篇博客: Java.concurrent.locks(1)-AQSJava.concurrent.locks(2)-ReentrantLock
而后如今已经晚上10点了,爸爸要回家睡觉了。同步块的部分之后想起了再更吧。那不过是用c艹实现的版本,原理一致,代码几乎也差很少。
相关文章
相关标签/搜索