Java 有了 synchronized,为什么还要提供 Lock
本文分享自华为云社区《Java中提供了synchronized,为什么还要提供Lock呢?》,作者: 冰 河。
在 Java 中提供了 synchronized 关键字来保证只有一个线程能够访问同步代码块。既然已经提供了 synchronized 关键字,那为何在 Java 的 SDK 包中,还会提供 Lock 接口呢?这是不是重复造轮子,多此一举呢?今天,我们就一起来探讨下这个问题。
再造轮子?
既然 JVM 中提供了 synchronized 关键字来保证只有一个线程能够访问同步代码块,为何还要提供 Lock 接口呢?这是在重复造轮子吗?Java 的设计者们为何要这样做呢?让我们一起带着疑问往下看。
为何提供 Lock 接口?
很多小伙伴可能会听说过,在 Java 1.5 版本中,synchronized 的性能不如 Lock,但在 Java 1.6 版本之后,synchronized 做了很多优化,性能提升了不少。那既然 synchronized 关键字的性能已经提升了,那为何还要使用 Lock 呢?
如果我们向更深层次思考的话,就不难想到了:我们使用 synchronized 加锁是无法主动释放锁的,这就会涉及到死锁的问题。
死锁问题
如果要发生死锁,则必须存在以下四个必要条件,四者缺一不可。
互斥条件
在一段时间内某资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。
不可剥夺条件
线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
请求与保持条件
线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放。
循环等待条件
在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中 P1 等待 P2 占有的资源,P2 等待 P3 占有的资源,…,Pn 等待 P1 占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所深情地资源。
synchronized 的局限性
如果我们的程序使用 synchronized 关键字发生了死锁时,synchronized 关键是是无法破坏“不可剥夺”这个死锁的条件的。这是因为 synchronized 申请资源的时候, 如果申请不到, 线程直接进入阻塞状态了, 而线程进入阻塞状态, 啥都干不了, 也释放不了线程已经占有的资源。
然而,在大部分场景下,我们都是希望“不可剥夺”这个条件能够被破坏。也就是说对于“不可剥夺”这个条件,占用部分资源的线程进一步申请其他资源时, 如果申请不到, 可以主动释放它占有的资源, 这样不可剥夺这个条件就破坏掉了。
如果我们自己重新设计锁来解决 synchronized 的问题,我们该如何设计呢?
解决问题
了解了 synchronized 的局限性之后,如果是让我们自己实现一把同步锁,我们该如何设计呢?也就是说,我们在设计锁的时候,要如何解决 synchronized 的局限性问题呢?这里,我觉得可以从三个方面来思考这个问题。
(1)能够响应中断。 synchronized 的问题是, 持有锁 A 后, 如果尝试获取锁 B 失败, 那么线程就进入阻塞状态, 一旦发生死锁, 就没有任何机会来唤醒阻塞的线程。 但如果阻塞状态的线程能够响应中断信号, 也就是说当我们给阻塞的线程发送中断信号的时候, 能够唤醒它, 那它就有机会释放曾经持有的锁 A。 这样就破坏了不可剥夺条件了。
(2)支持超时。 如果线程在一段时间之内没有获取到锁, 不是进入阻塞状态, 而是返回一个错误, 那这个线程也有机会释放曾经持有的锁。 这样也能破坏不可剥夺条件。
(3)非阻塞地获取锁。 如果尝试获取锁失败, 并不进入阻塞状态, 而是直接返回, 那这个线程也有机会释放曾经持有的锁。 这样也能破坏不可剥夺条件。
体现在 Lock 接口上,就是 Lock 接口提供的三个方法,如下所示。
lockInterruptibly()
支持中断。
tryLock()方法
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回 true,如果获取失败(即锁已被其他线程获取),则返回 false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法
tryLock(long time, TimeUnit unit)方法和 tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回 false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回 true。
也就是说,对于死锁问题,Lock 能够破坏不可剥夺的条件,例如,我们下面的程序代码就破坏了死锁的不可剥夺的条件。
例外,Lock 下面有一个 ReentrantLock,而 ReentrantLock 支持公平锁和非公平锁。
在使用 ReentrantLock 的时候, ReentrantLock 中有两个构造函数, 一个是无参构造函数, 一个是传入 fair 参数的构造函数。 fair 参数代表的是锁的公平策略, 如果传入 true 就表示需要构造一个公平锁, 反之则表示要构造一个非公平锁。如下代码片段所示。
锁的实现在本质上都对应着一个入口等待队列, 如果一个线程没有获得锁, 就会进入等待队列, 当有线程释放锁的时候, 就需要从等待队列中唤醒一个等待的线程。 如果是公平锁, 唤醒的策略就是谁等待的时间长, 就唤醒谁, 很公平; 如果是非公平锁, 则不提供这个公平保证, 有可能等待时间短的线程反而先被唤醒。 而 Lock 是支持公平锁的,synchronized 不支持公平锁。
最后,值得注意的是,在使用 Lock 加锁时,一定要在 finally{}代码块中释放锁,例如,下面的代码片段所示。
注:其他 synchronized 和 Lock 的详细说明,小伙伴们自行查阅即可。
版权声明: 本文为 InfoQ 作者【华为云开发者社区】的原创文章。
原文链接:【http://xie.infoq.cn/article/d00e667b134c06543d4bf803b】。文章转载请联系作者。
评论