作者:知乎用户
链接: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中找到这段话:

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,抽象方法,由子类实现,子类通过控制原子变量来表示是否获取锁成功,类似于上文代码的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。

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

对这个类比较感兴趣的小朋友可以参考爸爸的两篇博客:Java.concurrent.locks(1)-AQSJava.concurrent.locks(2)-ReentrantLock

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

Reentrant 可重入解释的更多相关文章

  1. reentrant可重入函数

    在多任务操作系统环境中,应用程序的各个任务是并发运行的,所以会经常出现多个任务“同时”调用同一个函数的情况.这里之所以在“同时” 这个词上使用了引号,是因为这个歌”同时“的含义与我们平时所说的同时不是 ...

  2. 可重入排他锁ReentrantLock源码浅析

    1.引子 "ReentrantLock"单词中的“Reentrant”就是“重入”的意思,正如其名,ReentrantLock是一个支持重入的排他锁,即同一个线程中可以多次获得同步 ...

  3. (转载)可重入函数(reentrant function)

    (转载)http://blog.163.com/xu_jin_rong/blog/static/1491966220086775017178 由于cublog系统的缘故,将前段时间写的一篇blog文章 ...

  4. Writing Reentrant and Thread-Safe Code(译:编写可重入和线程安全的代码)

    Writing Reentrant and Thread-Safe Code 编写可重入和线程安全的代码 (http://www.ualberta.ca/dept/chemeng/AIX-43/sha ...

  5. Use Reentrant Functions for Safer Signal Handling(译:使用可重入函数进行更安全的信号处理)

    Use Reentrant Functions for Safer Signal Handling 使用可重入函数进行更安全的信号处理 How and when to employ reentranc ...

  6. 函数可重入问题reentrant functions(函数执行过程中可以被中断,允许多个副本)

    最近经常听到这个名词,以前也听到过,不过接触更多的是“线程安全问题”,而且本人也一直理解的是两个名字的含义是一样的.今天仔细总结一下这个名词相关的概念. 引用博文:可重入函数和不可重入函数 (http ...

  7. 可重入函数reentrant function

    可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数:而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能 ...

  8. KEILC51可重入函数及模拟栈浅析

    MARK:文章中的红色部分是个人的理解. KEILC51可重入函数及模拟栈浅析 关键字:keilc51,模拟堆栈,可重入函数调用,参数传递,C?XBP,C?ADDXBP 摘要:本文较详细的介绍了kei ...

  9. synchronized 是可重入锁吗?为什么?

    什么是可重入锁? 关于什么是可重入锁,我们先来看一段维基百科的定义. 若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(re ...

随机推荐

  1. HDU 3555 Bomb(数位DP模板啊两种形式)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3555 Problem Description The counter-terrorists found ...

  2. Unix网络编程之环境搭建

    环境搭建 在尝试UNP书上的样例时,会由于找不到"unp.h"等问题无法编译成功.因此在学习之前须要先搭建好编译环境. 步骤 <1>下载  UNIX网络编程源码unpv ...

  3. Lesson 2 Building your first web page: Part 3

    Time to build your first HTML page by hand I could go on with more theory and send half of you to sl ...

  4. Linux 下实现虚拟光驱功能,查看iso文件内容

    1,创建挂载点(也可以不创建,直接用现有的目录) openSUSE:~ # mkdir /mnt/iso 2,挂载ISO文件至创建的挂载点 openSUSE:~ # mount -t iso9660 ...

  5. RAID信息存放位置!

    今天偶然的机会,客户打电话说有一台DELL T110的服务器换了主板电池RAID信息没了进不去系统了,问我怎么处理,T110的RAID是主板集成的S100的RAID卡(算是软RAID,通过BIOS配置 ...

  6. 网络地图WebMap介绍

    WebMap是从ArcGIS Online或者ArcGIS for Portal item上获取显示到用户的界面中. 需要的是地图的ID. 创建一个新的网络地图需要设置ID号,然后再用地图底图MapV ...

  7. 入门Python:《趣学Python编程》中英文PDF+代码

    入门python推荐学习<趣学python编程>,语言轻松,通俗易懂,讲解由浅入深,力求将读者阅读和学习的难度降到最低.任何对计算机编程有兴趣的人或者首次接触编程的人,不论孩子还是成人,都 ...

  8. 【RHEL7/CentOS7网络配置】

    目录 网卡配置文件 查网卡信息 测试网络是否正常 使用 nmtui 命令配置网络 使用 nm-connection-editor 工具配置网络 修改回6.x版本的网卡名 Rhel/CentOS网络配置 ...

  9. python数据处理技巧一

    字符串赋值(传参)技巧 Python中一般的字符串赋值的方式如下: variable = "Test" print "I just [%s] unit"%var ...

  10. 紫书 例题 9-9 UVa 10003 (区间dp+递推顺序)

    区间dp,可以以一个区间为状态,f[i][j]是第i个切点到第j个切点的木棍的最小费用 那么对于当前这一个区间,枚举切点k, 可以得出f[i][j] = min{dp(i, k) + dp(k, j) ...