ReentrantLock和synchronized的比较。Monitor是原理和作用。
开始
今天,我们来聊聊ReentrantLock和synchronized的相似与不同。
都是阻塞
ReentrantLock和synchronized都是加锁式同步,当一个线程获取了对象锁后,其它要进入同步块的线程就必须阻塞在同步块外等待。线程的阻塞和唤醒需要操作系统在用户态和内核态之间切换,所以,ReentrantLock和synchronized都是代价比较高的。
实现方式
synchronized是java语言的关键字,它的锁机制是由jvm实现的,是原生语法层面上的互斥,最底层是mutex。而ReentrantLock则是JDK1.5之后,提供的api层面 的锁,需要在代码中显示调用lock、unlock等方法来完成。
所以,从便利性来说,synchronized使用起来更简单一些。但是从灵活度来说,ReentrantLock更灵活,可控性也更强,可实现更细粒度的锁。但是,在使用ReentrantLock时,一定要注意lock和unlock的匹配和顺序,否则就可能造成死锁。常见的方案是把unlock放在异常处理的finally语句块中。
性能
人们很容易被大众化的观点所误导,认为synchronized的效率会比ReentrantLock差很多。但是事实上,synchronized在JDK的发展过程中,经过了不断优化,比如引入了偏向锁,轻量级锁,锁升级机制等,目前,已经和ReentrantLock的效率相差不多了。如果没有特殊的场景,推荐使用synchronized,因为它使用起来比较简单,且不会造成死锁。
是否公平锁
排队等厕所,厕所门上有把锁。里面的人用完出来,把钥匙给队伍最前面的人,这就是公平锁。如果里面的人用完出来,把钥匙直接扔地上,谁抢上算谁的,这就是非公平锁。
synchronized是非公平锁,并且它无法实现公平锁。要实现公平锁,可以通过ReentrantLock来实现。通过new ReentrantLock(true)可以用来构造一个公平锁。
是否可重入锁
一个线程可以对某个资源重复加锁,称之为可重入锁。这个情形很常见于递归。如果锁不可重入,就有可能会发生如下情况:
A线程获取方法B的锁,在方法B中,有代码递归调用了自己。于是,A线程需要在方法B中再次获取B的锁。如果锁不可重入,A就会发现,方法B上已经有锁,A就进入了等待。但事实上,给B加锁的就是A自己。自己一直在等待自己,岂不是可笑?
synchronized就是一把可重入锁。当然了,使用ReentrantLock也可以实现可重入锁。
Monitor
同步块 上图是一个同步块在执行时的基本示意图。单位时间里,只能有一个线程在同步块中。那么,一个线程在获得了同步块的执行权(即锁)之后,是否能够一直执行到线程完毕呢?我们说,可能不行。因为对某个资源的锁是有一个优先级的。正在执行的线程可能需要让位给优先级更高的线程,此时,当前线程就会进入等待区。所以,上图的,我们发现,有两块区域都是为等待的线程设置的,一个是Entry Set,另一个是Wait Set。假设当前同步块有代码正在运行,那么,新进入的线程就会进入Entry Set,被挤占被迫等待的线程,则会进入Wait Set。
图中还有一个含义是,当同步块中的线程执行完毕,退出同步块后,Entry Set和Wait Set中的线程会共同竞争,以获得同步块的执行权,即获取锁。
Monitor,直译为监视器,底层实现是Mutex。JVM会给每个对象和class字节码设置一个monitor。当某个线程要进入某个同步块是,就需要获得对应目标的monitor。换句话,当某个线程获得了对应目标的monitor,它就进入了同步块。当该线程执行完同步块,或被挤占而等待时,就会让出monitor。
synchronized就是利用monitor来实现的。
等待可中断
等待可中断是使用ReentrantLock时,可以实现的一个机制。当某个线程等待锁过长时间时,程序可以通过lockInterruptibly方法来使当前线程中断等待,转去执行其它的线程。
线程分组唤醒
有些场景下,我们可能不希望唤醒所有的线程,而是唤醒部分线程。这种方式在synchronized下是无法实现的。但是,ReentrantLock通过提供一个Contition类,可以同时绑定多个对象,以此,来实现线程的分组唤醒。