在Java5之前,在协调对象的访问时可以使用的机制只有synchronized(内置锁)和volatile(可保证可见性与防止指令重排)。Java5新增了一种新的机制:ReentrantLock。但这并不是内置锁的替代方法,而是当内置锁不适用时,作为一种可选择的高级功能。
一、Lock与ReentrantLock
Lock是一个接口,从名字可以看出这个接口表示的是“锁”,其方法如下图所示:
Lock接口
这个接口定义了一组抽象的加锁操作,lock()即加锁,unlock()表示解锁,tryLock()表示尝试获取锁,它也有一个可以设置超时时间的重载,还有其他一些方法,从名字可以看出其作用,不再赘述。
ReentrantLock实现了Lock,为Lock中的抽象方法提供了具体实现,其实现是借助了AQS,提供了与内置锁相同的互斥性和内存可见性,同时与内置锁具有相同的语义。从名称上不难看出ReentrantLock是可重入的锁。那为什么需要ReentrantLock呢?最开始的时候提到过,ReentrantLock不是内置锁的替代方案,而是当内置锁不适用的时候,作为一种可选择的方式,内置锁是有一些局限性的,例如不可响应中断。灵活性也是使用显式锁的一个原因, 使用显式锁,可以很明确的让几乎任何代码段称为临界区,这种方式可以使得锁的粒度可以改变,也就意味着我们可以很轻易的控制所的粒度,从而提供性能。下面是使用显式锁的基本框架:
Lock lock = new ReentrantLock();
lock.lock(); //加锁
try {
//更新一些状态
dosomething();
} finally {
lock.unlock();
}
使用显式锁的时候一定要注意在完成任务之后必须要解锁,否则这个锁将不能被其他线程获取到。而如何保证一定会执行到unlock这句代码呢(之前可能会有异常)?这个问题我在面试中被问到过(当时一时答不上来,面试官耐心引导我好久,才能答得上来),最简单的方式就是使用try-finally结构,在finally中做解锁操作。
轮询和定时
之前的文章有提到过,死锁在并发程序中很有可能发生,且Java并没有任何机制使得程序从死锁状态中恢复过来,只能重启程序。而死锁的一个表现就是“无限期的等待”,解决这个问题的一种方式就是使用定时的方式,如果获取锁时,线程被阻塞的时间超过设置的超时时间,线程就会被唤醒,并继续执行。在大多数JVM中,线程因为无法获取到锁而等待的这段时间里,是在不断的轮询,而不是挂起线程。
可中断的锁获取操作
内置锁的一个特性是无法被中断,而ReentrantLock可以在线程等待的这段时间里响应中断,这又为避免死锁等活跃性问题提供了解决方案。
使用lockInterruptibly()可以使得获取锁的操作是可被中断的,这个方法会抛出InterruptedException异常,所以我们在使用的时候需要做一些处理。同时trylock()也是可被中断的。
二、性能因素
在Java5中,显示锁的性能会优于内置锁,Java6使用改进后的算法来管理内置锁,使得内置锁的性能有所提高。下面是Java5和Java6中内置锁和显式锁的性能对比图:
性能对比图从上可以看出,Java6中内置锁的性能较Java5有很大的提高的,已经非常接近显示锁。(且在最新的Java中又进一步提高了)。仔细分析这张图,可以得出很多有意思的结论。
需要注意的是:性能是一个不断变化的指标,今天测试的结果也许在明天就不一样了。所以不能因为某次小测试,就盲目的选择某种解决方案。
三、公平性
显式锁允许设置获取锁是否是公平的(在构造函数中设置),默认是非公平的,内置锁是非公平锁。公平的意义是每个线程获取锁的机会是相同的,不会出现“插队”等现象。这种机制有时候是很有用的,但会使得性能有所降低,如果不是非常必要,不推荐使用公平模式。下面是公平锁和非公平锁的性能比较图:
性能比较图可见,在竞争激烈的场景中,公平锁的性能比非公平锁的性能要差很多。为什么会出现这种情况呢?一个原因是:在恢复一个被阻塞的线程与该线程真正开始运行之间存在严重的延迟。例如A线程准备释放锁,B线程将被唤醒,同时C线程也在申请这个锁,那么C线程很可能在B线程完全醒来的之前获得,使用,并释放这个锁,然后B再获取锁。这样的情况使得吞吐率大大提升。在这个场景中,好像是C插队了,比B先拿到了锁,如果这个锁是公平锁,将会使得C无法获取到锁,只能先等待B醒来,获取锁,使用锁,释放锁,最终导致B,C完成任何的时间比在非公平锁的模式下更长。
四、在内置锁和显式锁之间进行选择
显式锁和内置锁在加锁,解锁语义上是相同的,显示锁提供了很多强劲的功能,例如可响应中断,可设置公平性,较内置锁灵活,性能在很多情况下优于内置锁等等.....但这是不是意味着我们总是使用显式锁来保护程序的状态呢?事情总是有两面性,不是非黑即白的,不是吗?下面是一个有助于选择哪个方案的指导性原则:
在一些内置锁无法满足需求的情况下,显式锁可以作为一种高级工具。当需要一些高级功能时才应该使用显式锁,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是优先使用内置锁。
在最新的Java版本中,又改进来内置锁的算法,使得内置锁的效率再次提升,这是否意味着Java会向着提高内置锁性能的方向发展呢?
五、读写锁
ReentrantLock是标准的互斥锁,即同一时间只允许一个线程进入临界区,无论这个操作是读操作还是写操作。这算是比较强硬的加锁规则,比较保守,因此也不必要的限制了并发性。ReadWriteLock解决了这个问题,具有“读-读不互斥,读-写互斥,写-写互斥”的特性,在读多写少的场景中尤其适用。下面是ReadWriteLock的接口方法:
ReadWriteLock
只有两个,即获取读锁和获取写锁。其有一个实现类是ReentrantReadWriteLock,使用起来比较简单,下面是读-写锁和ReentrantLock之间的性能对比图:
读-写锁性能
小结
与内置锁相比,显式锁提供了很多功能,具有更高的灵活性,并且对队列有着更好的控制。但显式锁不是内置锁的替代,只有在内置锁不满足需求时,才应该使用它。
网友评论