Reentrant 可重入解释
链接: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();
}
一共四步:
- tryAcquire,抽象方法,由子类实现,子类通过控制原子变量来表示是否获取锁成功,类似于上文代码的Step1、Step2
- addWaiter,已经实现的方法,表示将当前线程加入等待队列,类似于上文的Step3
- acquireQueued(),挂起线程,唤醒后重试,类似于上文的Step4、Step5
- 处理线程中断标志位。
我们只需要记住一个重要的地方就是,子类只需要实现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)-AQS、Java.concurrent.locks(2)-ReentrantLock。
然后现在已经晚上10点了,爸爸要回家睡觉了。同步块的部分以后想起了再更吧。那不过是用c艹实现的版本,原理一致,代码几乎也差不多。
Reentrant 可重入解释的更多相关文章
- reentrant可重入函数
在多任务操作系统环境中,应用程序的各个任务是并发运行的,所以会经常出现多个任务“同时”调用同一个函数的情况.这里之所以在“同时” 这个词上使用了引号,是因为这个歌”同时“的含义与我们平时所说的同时不是 ...
- 可重入排他锁ReentrantLock源码浅析
1.引子 "ReentrantLock"单词中的“Reentrant”就是“重入”的意思,正如其名,ReentrantLock是一个支持重入的排他锁,即同一个线程中可以多次获得同步 ...
- (转载)可重入函数(reentrant function)
(转载)http://blog.163.com/xu_jin_rong/blog/static/1491966220086775017178 由于cublog系统的缘故,将前段时间写的一篇blog文章 ...
- Writing Reentrant and Thread-Safe Code(译:编写可重入和线程安全的代码)
Writing Reentrant and Thread-Safe Code 编写可重入和线程安全的代码 (http://www.ualberta.ca/dept/chemeng/AIX-43/sha ...
- Use Reentrant Functions for Safer Signal Handling(译:使用可重入函数进行更安全的信号处理)
Use Reentrant Functions for Safer Signal Handling 使用可重入函数进行更安全的信号处理 How and when to employ reentranc ...
- 函数可重入问题reentrant functions(函数执行过程中可以被中断,允许多个副本)
最近经常听到这个名词,以前也听到过,不过接触更多的是“线程安全问题”,而且本人也一直理解的是两个名字的含义是一样的.今天仔细总结一下这个名词相关的概念. 引用博文:可重入函数和不可重入函数 (http ...
- 可重入函数reentrant function
可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数:而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能 ...
- KEILC51可重入函数及模拟栈浅析
MARK:文章中的红色部分是个人的理解. KEILC51可重入函数及模拟栈浅析 关键字:keilc51,模拟堆栈,可重入函数调用,参数传递,C?XBP,C?ADDXBP 摘要:本文较详细的介绍了kei ...
- synchronized 是可重入锁吗?为什么?
什么是可重入锁? 关于什么是可重入锁,我们先来看一段维基百科的定义. 若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(re ...
随机推荐
- WordPress改动新用户注冊邮件内容--自己定义插件
有些开放用户注冊功能的WordPress站点,可能有这么一项需求,就是用户注冊成功后,系统会分别给站点管理员和新用户发送一封通知邮件.给管理员发送的是新用户的username和Email,给刚刚注冊的 ...
- USACO2011 Jan:公司利润
简要题意: 奶牛开了家公司,已经连续运作了N 天.它们在第i 天获得了Ai元的利润,不过有些天是亏钱的,这种情况下利润就是一个负数.约翰想为它们写个新闻,吹嘘它们的惊人业绩.请你帮助他选出一段连续的日 ...
- Android Design Support Library初探,NavigationView实践
前言 在前几天的IO大会上,Google带来了Android M,同时还有Android支持库的新一轮更新,其中更是增加一个全新的支持库Android Design Support Library,包 ...
- git帮助命令
git帮助命令 零.自己实例 cd D://software/code/PHP/phpStudy/PHPTutorial/WWW/github/m_Orchestrate git checkout - ...
- 之前搭建的jenkins的一些笔记
wget -O /etc/yum.repos.d/jenkins.repo http://jenkins-ci.org/redhat/jenkins.repo rpm --import http:// ...
- 深入Vue的响应式原理
工作的过程中,有时候会有数据改变但是视图没有更新的问题,作者在vue的官方文档中有提到这个问题,我来总结一下 1.vue的每个组件实例都有对象的watcher实例对象,它会在组件渲染的过程中把属性记录 ...
- Codeforces 344A Magnets
Description Mad scientist Mike entertains himself by arranging rows of dominoes. He doesn't need dom ...
- linux下安装配置rabbitMQ
1.安装Erlang 由于RabbitMQ依赖Erlang, 所以需要先安装Erlang Erlang的安装方式大概有两种: 1.从Erlang Solution安装(推荐) # 添加erlang s ...
- C++虚表的原理,很好
下面这篇文章讲的很好. http://www.cnblogs.com/lihaosky/articles/1606502.html 假设我们有这样的一个类: class Base { public: ...
- [Python] Format Strings in Python
Single quotes and double quotes can both be used to declare strings in Python. You can even use trip ...