作者:知乎用户
链接:https://www.zhihu.com/question/37168009/answer/88086943
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

我们来看看问题,按照现在我看到的情况,题干是:“怎样证明synchronized锁,Lock锁是可重入的”,外加一个Java的标签。

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

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

我们第一步要明确什么是“可重入的”。其对应的英文单词是:Reentrant,哦不对,其实准确的说应该是“Re-entrant”。wikipedia有一个Reentrancy(computing)的解释。不过在ReentrantLock的doc中找到这段话:

最后一句话尤其重要,如果当前占用这个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()函数来唤醒等待队列头部的线程
  • 唤醒之后,线程继续试着获取锁,失败则递归,成功则返回

慢着,知道上面的东西,离我们证明题干还有一定的距离,继续看。

就是这个小朋友,归纳总结出,嗯各种同步手段底层都需要一些共同的东西,所以写了一个类叫java.util.concurrent.locks.AbstractQueuedSynchronizer。后来被简称为AQS框架,该框架将加锁的步骤模板化了之后,提供了基本的列表、状态控制等等手段。我们可以简单看看lock的过程他是如何抽象的:

 public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

一共四步:

  1. tryAcquire,抽象方法,由子类实现,子类通过控制原子变量来表示是否获取锁成功,类似于上文代码的Step1、Step2
  2. addWaiter,已经实现的方法,表示将当前线程加入等待队列,类似于上文的Step3
  3. acquireQueued(),挂起线程,唤醒后重试,类似于上文的Step4、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第二次进来的时候,再次控制这个原子变量,发现就不好使了,就进入等待队列。自己就被自己等死了。

好,最后就是重点,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。

我想差不多应该可以证明了吧..

然后现在已经晚上10点了,爸爸要回家睡觉了。同步块的部分以后想起了再更吧。那不过是用c艹实现的版本,原理一致,代码几乎也差不多。

05-16 12:36