浅谈可重入性及其他

点击量:428

这篇文章主要讨论三个问题:1.什么是可重入性 2.什么是可重入锁 3.什么是ReentrantLock
首先为什么会讨论可重入性呢?大家知道java中有一个非常常用的类库叫ReentrantLock,中文翻译成可重入锁,每次看到这个翻译我的第一反应就是什么是可重入锁?为什么要这么命名呢?那么什么是ReentrantLock呢?Oracle的官方文档给出的解释是这样的:

A reentrant mutual exclusion Lock with the same basic behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements, but with extended capabilities.

意思就是ReentrantLock和synchronized关键字所起的基本作用是一样的,但是提供了更多的扩展功能。看了这个解释并没有解答我的困惑,官方文档并没有解释什么是可重入锁,只是说了ReentrantLock提供了更多的功能。那么什么是可重入锁呢?
在这之前我们先来弄清楚一个概念:
可重入性

若一个程序或子程序可以“安全的被并行执行(Parallel computing)”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,可以再次进入并执行它(并行执行时,个别的执行结果,都符合设计时的预期)。

大致的意思就是:如果一个函数能被安全的重复执行,那么这个函数就是可重入的。这里的安全包括数据一致性,程序安全(不会死锁,饥饿等)等。这个概念理解起来其实跟线程安全很相似,但是从定义的角度来说这两者又是不一样的。可重入的函数不一定是线程安全的,比如当多个线程同时读取某个数据时,因为不涉及到写,所以多次读取肯定是可重入的,但是这不是线程安全的,因为有其他线程可能会修改这个数据。同样的线程安全的函数也并不一定是可重入的,看下面的代码:

这个方法是线程安全的,但是他不是可重入的,为什么呢?如果一个线程执行function,在拿到锁之后还没unlock就return的时候,另一个线程也调用function这个函数,结果就会一直等锁,导致starvation。

可重入锁

可重入锁:Reentrant means that a thread that already holds a lock can retake it,这句话的意思是一个线程可以重复获得他已经拥有的锁,反过来说就是不可重入锁是不能被重复获得的!也就是说你获得一个不可重入锁之后,再想去获得这个锁就可能会有死锁,饿死等情况出现,导致程序崩溃。在解释什么样的锁设计是不可重入锁之前,我们先思考下为什么我们要重复获得同一个锁呢?当前线程已经拿到那个锁了,为什么还要再次获取呢?原因是随着业务需求的不断增长和复杂性的增加,方法嵌套获取锁已经很难避免了,举个例子:

方法A和方法B都锁同一个锁,但是他们的逻辑功能不一样,如果synchronized的实现不是可重入的,那么这段代码就是有问题的,但是从代码层面很难分析出这个问题,因此我们要使用可重入锁。
那么什么样设计的锁是不可重入锁呢?看下面的代码:

如果一个线程调用了两次lock,但第一次没有调用unlock,第二次再调用lock时会导致线程在等待阻塞状态,这样设计的锁就是不可重入的,也就是说一个线程不能安全的重复获取的同一个锁,到这里应该明白什么是可重入锁了,而java并发库里的synchronized和ReentrantLock都是可重入锁,它们只是可重入锁的一种实现!

什么是ReentrantLock

以下这内容主要参考:https://dzone.com/articles/what-are-reentrant-locks
ReentrantLock是在java5.0之后提出来,之所以被提出来是因为synchronized关键字提供的同步机制有以下几个缺点:
1.不能中断一个正在等锁的线程
2.一旦某个线程开始等锁,那么它将一直等下去
3.不能在非块状结构的代码中使用

正是为了解决这些痛点,ReentrantLock横空出世,主要有以下高级功能:
1.non-block structure
ReentrantLock可以在不同的方法中使用(非块状即non-block structure),这一点synchronized做不到。
2.支持公平锁策略
可在构造函数中设置公平锁或者非公平锁。
公平锁:优先把锁给等待时间最长的线程
非公平锁:不保证等待线程拿锁的顺序
这里顺便提一下:公平锁的性能较差,为什么呢?
当一个线程在等待锁的时候,它的状态会被置成waiting状态,如果按照公平锁的的规则,优先给这个线程锁的话,那么就要恢复这个线程。我们假设有个新的线程想要获得锁,它的状态还没有被挂起,如果按照非公平锁的规则,则优先把锁给这个线程,这样就不用频繁的恢复唤起线程了。
3.支持中断锁
收到中断信号的时候可以释放拥有的锁。
4.支持定时锁
tryLock()
这个方法顾名思义就是尝试去获得锁,如果当前没有其他线程拥有这个锁,则会返回true,否则返回false。值得注意的是这个方法是会破坏公平队列的,也就是说即使你的锁设置的是公平锁,它也会破坏这个规则立即获得锁。如果你想遵守公平性的花可以使用tryLock(0,TimeUtil.SECONDS),效果和tryLock()是一样的。
tryLock(long,TimeUtil)这个方法可以指定等待的时间,遵循公平性规则,等待一定的时间,如果在该时间内获得锁,则返回true,否则返回false。
tryLock最常见的用法是解决死锁,接下来看个例子,如何使用tryLock来解决死锁问题。

上面这段代码,如果同时有两个线程,Account参数相反的话就有可能产生死锁。比如:
A: transferMoney(acc1, acc2, 20);
B: transferMoney(acc2, acc1 ,25);
那怎么使用tryLock解决呢?

这段代码就是在想把两个锁都同时拿到,如果拿不到,则不执行真正的操作,然后释放锁,等待下一轮再次尝试。这种方式被称作:“Polled and Timed Lock Acquisition”,就是轮询定时获取锁。

THE END.

References:
wikipedia-可重入性
reentrance-lockout
https://docs.oracle.com/javase/6/docs/api/java/util/concurrent/locks/ReentrantLock.html
https://dzone.com/articles/what-are-reentrant-locks

浅谈可重入性及其他》上有1条评论

  1. Pingback引用通告: ReentrantLock VS Synchronized – WB

发表评论

电子邮件地址不会被公开。 必填项已用*标注