在jdk5之前,协调共享对象访问的只有synchronized和lock,jdk增加了一种新的锁机制:ReentrantLock。lock并不是对内置锁的替换,而是互补。这篇文章主要循序渐进的比较两者异同和适用场景,如果有理解错的地方希望大家能指出。
synchronized怎么实现的?
synchronized是Java中解决并发问题的一种最常用最简单的一种方法,有效满足了线程安全的三大要求:原子性、可见性和有序性。
synchronized可以用在三个地方:
- 修饰普通方法。锁的是实例对象。
- 修饰静态方法。锁的是类。
- 修饰代码块。锁的是括号内的对象。
synchronized是依靠对象头中一个monitor(监视器锁)。执行synchronized修饰的方法或者代码块的时候,会首先去尝试获取这个monitor,如果monitor获取成功,那么就可以执行。否则就必须等待,一直到可以获取monitor。synchronized的可重入性也就好解释了,如果一个线程已经获取monitor,再次获取会将计数器加一,而不用再去等待锁。退出的时候计数器会减1,当减到0的时候,就可以唤醒其他等待的线程。当然这些实现JVM都已经帮我们打理好了,所以我们会觉得synchronized用起来非常简单。
为什么要用ReentrantLock?
前面介绍了synchronized,使用方便简单。所谓成也萧何败也萧何,太简单了就会有一些问题:
- 无法中断一个已经正在等待获取锁的线程。如果一个线程一直没法获取锁,就会一直等待下去。ReentrantLock可以使用中断和定时锁,等待一定时间如果没有获取就返回。
- 无法对加锁规则进行改动,无法实现非阻塞结构的加锁规则。
- synchronized如果遇到死锁问题,恢复的解决方案就是重新启动程序。
- 很多场景,读与读并不需要互斥,而synchronized粗暴的将所有操作都互斥。ReadWriteLock可以实现读-写锁,使用两个Lock对象,一个负责读一个负责写。
- synchronized是非公平锁,无法适应需要公平锁的场景。ReentrantLock分为公平锁和非公平锁。
所谓公平锁就是每次唤醒的时候,按照FCFS原则唤醒等待队列中的线程。而非公平锁是唤醒的时候,其他等待的线程一起去抢锁,后来的线程可能比先到的线程要先拿到锁,可以插队,这也就是非公平的体现。
在激烈竞争的情况下,非公平锁的性能高于公平性,是因为插队可以带来吞吐量的提升。举例来说,线程A持有锁,线程B在等待锁。此时线程A是释放锁,B将被唤醒,然后尝试获取锁。此时线程C很可能在C没有被完全唤醒之前获取这个锁,锁被更早的获得了,当然也就提高了吞吐量。
相对应的ReentrantLock的特点就是:
- 可中断,可定时,缓解了死锁的问题。
- 公平锁,可以实现线程先来的先获取锁
- 可以有ReadWriteLock实现读写锁,读与读不必互斥。
总结一句就是,synchronized不够灵活,这也是ReentrantLock出现的原因。一个很好的例子就是锁分段技术,比如在ConcurrentHashMap
中是划分segment来将锁分段,提高并发环境下的性能,而源码中就是用的ReenterantLock。
/**
* Stripped-down version of helper class used in previous version,
* declared for the sake of serialization compatibility
*/
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}
什么时候用ReentrantLock,什么时候用synchronized?
ReentrantLock带来了使用的灵活性的代价就是,你需要仔细负责lock的操作,比如unlock,加锁就有释放,用起来自然就没有synchronized那么省心。
synchronized在java6有了一些升级,所以两者的性能事实上差距不大。所以私以为,如果需要对锁操作或者等待队列有着更高的灵活性或者需要锁是公平的,使用ReentrantLock。否则使用synchronized足矣。