多线程-可重入锁与不可重入锁

什么是可重入锁与不可重入锁?

可重入锁”这四个字分开来解释:

  • :可以。
  • :再次。
  • :进入。
  • :同步锁。

综上所述,“可重入锁”就是这把同步锁可以再次进入。

进入什么?

进入同步域(即同步代码块/方法或显式锁锁定的代码)

通俗来讲,可重入锁就是一证通

多线程-可重入锁与不可重入锁_第1张图片

 

只需一个证就可以通过所有相同关卡:

多线程-可重入锁与不可重入锁_第2张图片

 

不可重入锁就是:即使每个关卡相同,你也得再拿一个一摸一样的证件来

多线程-可重入锁与不可重入锁_第3张图片

 

如果把证件看作是同步锁,把关卡看作是同步域(即同步代码块/方法或显式锁锁定的代码),那么可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。

可重入锁和不可重入锁下面都有例子演示,希望大家可以结合例子体会可重入锁与不可重入锁的区别与联系。

看了不可重入锁的动画,大家心想:这不有病吗?已经有证件了,后面还要证件。所以,可重入锁就成了隐式锁和显式锁的默认选项。

隐式锁(即synchronized关键字使用的锁)默认是可重入锁,显式锁(即Lock)也有ReentrantLock这样的可重入锁。

单词Reentrant就是重入的意思,ReentrantLock即可重入锁

隐式锁,即synchronized关键字用的锁。在《“全栈2019”Java多线程第二十七章:Lock获取lock/释放unlock锁》一章中有介绍过,感兴趣的小伙伴可以前去阅读。

3.隐式锁可重入锁例子

我们先来看看隐式锁的可重入锁的例子。

隐式锁的可重入锁的例子很简单,这里我们先写一个匿名内部类实现Runnable接口的对象:

多线程-可重入锁与不可重入锁_第4张图片

 

先不着急把run()方法写好,先来把线程创建好。

需要锁的地方一定是多个线程,但是这里我们只需1个线程即可,因为只要演示锁可重入即可,不需要多个线程去竞争锁,所以这里创建1个线程,并把runnable对象传递给线程:

多线程-可重入锁与不可重入锁_第5张图片

 

紧接着,我们启动线程的代码也写了:

多线程-可重入锁与不可重入锁_第6张图片

 

接下来,我们来完善run()方法内部。

在run()方法内部需要做的是,同步调用同步,即同步代码块/方法内部执行同步代码块/方法。我们先写一个外层同步代码块/方法:

多线程-可重入锁与不可重入锁_第7张图片

 

其次是在同步代码块/方法内部去执行另一个同步代码块/方法:

多线程-可重入锁与不可重入锁_第8张图片

 

好了,例子写完了,运行程序,执行结果:

多线程-可重入锁与不可重入锁_第9张图片

 

从运行结果来看,符合预期。

为什么说符合预期?

因为只要同步递归调用不发生死锁就是可重入锁,在上例中,外层同步代码块嵌套内层同步代码块,满足同步递归调用,所以说隐式锁默认就是可重入锁。

4.显式锁可重入锁例子

显式锁即Lock。在上一章《“全栈2019”Java多线程第二十七章:Lock获取lock/释放unlock锁》中也有介绍过,感兴趣的小伙伴可以前去阅读。

下面我们就来用显式锁改写上一小节例子。

首先,我们创建出显式锁对象:

多线程-可重入锁与不可重入锁_第10张图片

 

其次,我们将外层同步代码块开始的地方换成lock.lock(),结束的地方换成lock.unlock()。即这两个地方:

多线程-可重入锁与不可重入锁_第11张图片

 

换完之后的样子:

多线程-可重入锁与不可重入锁_第12张图片

 

然后是将内层同步代码块开始的地方换成lock.lock(),结束的地方换成lock.unlock()。即这两个地方:

多线程-可重入锁与不可重入锁_第13张图片

 

换完之后的样子:

多线程-可重入锁与不可重入锁_第14张图片

 

好了,例子用显式锁改写完成。

接下来运行程序,观察执行结果:

多线程-可重入锁与不可重入锁_第15张图片

 

从运行结果来看,符合预期。

为什么说符合预期?

因为我们例子经过显式锁改写之后没有发生死锁,所以显式锁默认的也是可重入锁。

另外,该例子不光没有产生死锁,而且还更加直观的展现了可重入锁获取锁释放锁的流程

5.可重入锁的工作原理

可重入锁的工作原理很简单,就是用一个计数器来记录锁被获取的次数,获取锁一次计数器+1,释放锁一次计数器-1,当计数器为0时,表示锁可用。

我们简单的来模拟出这个工作原理。

先定义一个计数器变量:

多线程-可重入锁与不可重入锁_第16张图片

 

然后,在获取同步锁的地方让计数器+1,并输出该锁被获取的次数:

多线程-可重入锁与不可重入锁_第17张图片

 

接着,在释放锁的地方让计数器-1,并输出该锁被获取的次数:

多线程-可重入锁与不可重入锁_第18张图片

 

同理,在内层同步代码块也这么做:

多线程-可重入锁与不可重入锁_第19张图片

 

例子书写完毕,运行程序,执行结果:

多线程-可重入锁与不可重入锁_第20张图片

 

从运行结果来看,符合预期。

这里需要注意的是,我们同步域需要的锁一定要是同一把锁

6.不可重入锁例子

再看一遍不可重入锁的动画:

多线程-可重入锁与不可重入锁_第21张图片

 

可以看出不可重入锁是每一个同步域需要的锁即使一样,也要你重新获取锁。现有阶段的锁默认都是可重入锁。所以我们得自己造一把不可重入锁

首先,定义一个不可重入锁NotReentrantLock类:

多线程-可重入锁与不可重入锁_第22张图片

 

然后,实现Lock接口:

多线程-可重入锁与不可重入锁_第23张图片

 

接着,我们需要着重实现两个方法即可,一个lock()方法:

多线程-可重入锁与不可重入锁_第24张图片

 

还有一个unlock()方法:

多线程-可重入锁与不可重入锁_第25张图片

 

怎么实现lock()方法呢?

根据不可重入锁的动画和定义来看,只需将锁和一个线程绑定即可。

于是,我们在类中定义一个Thread类型的变量,用于绑定线程:

多线程-可重入锁与不可重入锁_第26张图片

 

紧接着,我们在lock()方法中记录当前来获取锁的线程:

多线程-可重入锁与不可重入锁_第27张图片

 

于是,线程绑定工作就做完了。

如果该线程已经拿到锁之后,还要再获取锁时,就给它wait,即不可重入

多线程-可重入锁与不可重入锁_第28张图片

 

lock()方法实现完毕。

那unlock()方法怎么实现呢?

如果当前线程为绑定线程时,我们将绑定线程变量thread置空即可

先获取当前线程:

多线程-可重入锁与不可重入锁_第29张图片

 

如果当前线程不为绑定线程,则使当前线程等待:

多线程-可重入锁与不可重入锁_第30张图片

 

如果当前线程为绑定线程,则将绑定线程变量置空:

多线程-可重入锁与不可重入锁_第31张图片

 

最后唤醒所有等待该锁的线程:

多线程-可重入锁与不可重入锁_第32张图片

 

至此,不可重入锁写完了。

下面,来用用自定义的不可重入锁。

还是上一小节的例子,只不过将ReentrantLock对象改为NotReentrantLock对象:

多线程-可重入锁与不可重入锁_第33张图片

 

例子改写完毕,运行程序,执行结果:

多线程-可重入锁与不可重入锁_第34张图片

 

从运行结果来看,符合预期。

外层同步域已经拿到锁了,它不能连续再拿第二次锁:

多线程-可重入锁与不可重入锁_第35张图片

 

接下来,外层同步域执行到内层同步域:

多线程-可重入锁与不可重入锁_第36张图片

 

此时内层同步域也需要和外层同步域一样的锁,于是就去执行lock.lock()方法,当发生当前线程就是绑定线程时,就wait了:

多线程-可重入锁与不可重入锁_第37张图片

 

于是,程序就停止不动。

7.不可重入锁别名

不可重入锁也叫自旋锁

 

总结

  • 可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
  • 隐式锁(即synchronized关键字使用的锁)默认是可重入锁,显式锁(即Lock)也有ReentrantLock这样的可重入锁。
  • 可重入锁的工作原理很简单,就是用一个计数器来记录锁被获取的次数,获取锁一次计数器+1,释放锁一次计数器-1,当计数器为0时,表示锁可用。
  • 不可重入锁也叫自旋锁。

你可能感兴趣的:(多线程-可重入锁与不可重入锁)