欢迎大家来到IT世界,在知识的湖畔探索吧!
难度
初级
学习时间
30分钟
适合人群
零基础
开发语言
Java
开发环境
- JDK v11
- IntelliJ IDEA v2018.3
友情提示
- 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
- 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!
1.温故知新
前面在《“全栈2019”Java多线程第二十四章:等待唤醒机制详解》一章中介绍了等待唤醒机制。
在《“全栈2019”Java多线程第二十五章:生产者与消费者线程详解》一章中介绍了生产者与消费者线程。
在《“全栈2019”Java多线程第二十六章:同步方法生产者与消费者线程》一章中介绍了用同步方法来实现生产者与消费者线程例子。
在《“全栈2019”Java多线程第二十七章:Lock获取lock/释放unlock锁》一章中介绍了Lock获取lock/释放unlock锁。
在《“全栈2019”Java多线程第二十八章:公平锁与非公平锁详解》一章中介绍了公平锁与非公平锁。
现在我们来讲解可重入锁与不可重入锁。
2.什么是可重入锁与不可重入锁?
“可重入锁”这四个字分开来解释:
- 可:可以。
- 重:再次。
- 入:进入。
- 锁:同步锁。
综上所述,“可重入锁”就是这把同步锁可以再次进入。
进入什么?
进入同步域(即同步代码块/方法或显式锁锁定的代码)。
通俗来讲,可重入锁就是一证通。
只需一个证就可以通过所有相同关卡:
不可重入锁就是:即使每个关卡相同,你也得再拿一个一摸一样的证件来:
如果把证件看作是同步锁,把关卡看作是同步域(即同步代码块/方法或显式锁锁定的代码),那么可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
可重入锁和不可重入锁下面都有例子演示,希望大家可以结合例子体会可重入锁与不可重入锁的区别与联系。
看了不可重入锁的动画,大家心想:这不有病吗?已经有证件了,后面还要证件。所以,可重入锁就成了隐式锁和显式锁的默认选项。
隐式锁(即synchronized关键字使用的锁)默认是可重入锁,显式锁(即Lock)也有ReentrantLock这样的可重入锁。
单词Reentrant就是重入的意思,ReentrantLock即可重入锁。
隐式锁,即synchronized关键字用的锁。在《“全栈2019”Java多线程第二十七章:Lock获取lock/释放unlock锁》一章中有介绍过,感兴趣的小伙伴可以前去阅读。
3.隐式锁可重入锁例子
我们先来看看隐式锁的可重入锁的例子。
隐式锁的可重入锁的例子很简单,这里我们先写一个匿名内部类实现Runnable接口的对象:
先不着急把run()方法写好,先来把线程创建好。
需要锁的地方一定是多个线程,但是这里我们只需1个线程即可,因为只要演示锁可重入即可,不需要多个线程去竞争锁,所以这里创建1个线程,并把runnable对象传递给线程:
紧接着,我们启动线程的代码也写了:
接下来,我们来完善run()方法内部。
在run()方法内部需要做的是,同步调用同步,即同步代码块/方法内部执行同步代码块/方法。我们先写一个外层同步代码块/方法:
其次是在同步代码块/方法内部去执行另一个同步代码块/方法:
好了,例子写完了,运行程序,执行结果:
从运行结果来看,符合预期。
为什么说符合预期?
因为只要同步递归调用不发生死锁就是可重入锁,在上例中,外层同步代码块嵌套内层同步代码块,满足同步递归调用,所以说隐式锁默认就是可重入锁。
4.显式锁可重入锁例子
显式锁即Lock。在上一章《“全栈2019”Java多线程第二十七章:Lock获取lock/释放unlock锁》中也有介绍过,感兴趣的小伙伴可以前去阅读。
下面我们就来用显式锁改写上一小节例子。
首先,我们创建出显式锁对象:
其次,我们将外层同步代码块开始的地方换成lock.lock(),结束的地方换成lock.unlock()。即这两个地方:
换完之后的样子:
然后是将内层同步代码块开始的地方换成lock.lock(),结束的地方换成lock.unlock()。即这两个地方:
换完之后的样子:
好了,例子用显式锁改写完成。
接下来运行程序,观察执行结果:
从运行结果来看,符合预期。
为什么说符合预期?
因为我们例子经过显式锁改写之后没有发生死锁,所以显式锁默认的也是可重入锁。
另外,该例子不光没有产生死锁,而且还更加直观的展现了可重入锁获取锁释放锁的流程。
5.可重入锁的工作原理
可重入锁的工作原理很简单,就是用一个计数器来记录锁被获取的次数,获取锁一次计数器+1,释放锁一次计数器-1,当计数器为0时,表示锁可用。
我们简单的来模拟出这个工作原理。
先定义一个计数器变量:
然后,在获取同步锁的地方让计数器+1,并输出该锁被获取的次数:
接着,在释放锁的地方让计数器-1,并输出该锁被获取的次数:
同理,在内层同步代码块也这么做:
例子书写完毕,运行程序,执行结果:
从运行结果来看,符合预期。
这里需要注意的是,我们同步域需要的锁一定要是同一把锁。
6.不可重入锁例子
再看一遍不可重入锁的动画:
可以看出不可重入锁是每一个同步域需要的锁即使一样,也要你重新获取锁。现有阶段的锁默认都是可重入锁。所以我们得自己造一把不可重入锁。
首先,定义一个不可重入锁NotReentrantLock类:
然后,实现Lock接口:
接着,我们需要着重实现两个方法即可,一个lock()方法:
还有一个unlock()方法:
怎么实现lock()方法呢?
根据不可重入锁的动画和定义来看,只需将锁和一个线程绑定即可。
于是,我们在类中定义一个Thread类型的变量,用于绑定线程:
紧接着,我们在lock()方法中记录当前来获取锁的线程:
于是,线程绑定工作就做完了。
如果该线程已经拿到锁之后,还要再获取锁时,就给它wait,即不可重入:
lock()方法实现完毕。
那unlock()方法怎么实现呢?
如果当前线程为绑定线程时,我们将绑定线程变量thread置空即可。
先获取当前线程:
如果当前线程不为绑定线程,则使当前线程等待:
如果当前线程为绑定线程,则将绑定线程变量置空:
最后唤醒所有等待该锁的线程:
至此,不可重入锁写完了。
下面,来用用自定义的不可重入锁。
还是上一小节的例子,只不过将ReentrantLock对象改为NotReentrantLock对象:
例子改写完毕,运行程序,执行结果:
从运行结果来看,符合预期。
外层同步域已经拿到锁了,它不能连续再拿第二次锁:
接下来,外层同步域执行到内层同步域:
此时内层同步域也需要和外层同步域一样的锁,于是就去执行lock.lock()方法,当发生当前线程就是绑定线程时,就wait了:
于是,程序就停止不动。
7.不可重入锁别名
不可重入锁也叫自旋锁。
最后,希望大家可以把这个例子照着写一遍,然后再自己默写一遍,方便以后碰到类似的面试题可以轻松应对。
祝大家编码愉快!
GitHub
本章程序GitHub地址:https://github.com/gorhaf/Java2019/tree/master/Thread/Lock
总结
- 可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
- 隐式锁(即synchronized关键字使用的锁)默认是可重入锁,显式锁(即Lock)也有ReentrantLock这样的可重入锁。
- 可重入锁的工作原理很简单,就是用一个计数器来记录锁被获取的次数,获取锁一次计数器+1,释放锁一次计数器-1,当计数器为0时,表示锁可用。
- 不可重入锁也叫自旋锁。
至此,Java中可重入锁与不可重入锁相关内容讲解先告一段落,更多内容请持续关注。
答疑
如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。
上一章
“全栈2019”Java多线程第二十八章:公平锁与非公平锁详解
下一章
“全栈2019”Java多线程第三十章:尝试获取锁tryLock()方法详解
学习小组
加入同步学习小组,共同交流与进步。
- 方式一:关注头条号Gorhaf,私信“Java学习小组”。
- 方式二:关注公众号Gorhaf,回复“Java学习小组”。
全栈工程师学习计划
关注我们,加入“全栈工程师学习计划”。
版权声明
原创不易,未经允许不得转载!
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/35253.html