ReentrantLock分析 及 与Synchronized对比分析
ReentrantLock概念、公平性
什么是再入?
它表示当一个线程试图获取一个它已经获取的锁时,这个获取动作就自动成功,这是对锁获取粒度的一个概念,也就是锁的持有是以线程为单位,而不是基于调用次数。Java锁实现强调再入性是为了和pthread的行为进行区分。
再入锁可以设置公平性,我们可以在创建再入锁时选择是否公平。
ReentrantLock fairLock = new RenntrantLock(true);
这里所指的公平性,是指在竞争场景中,当公平性为真时,会倾向于将锁赋予等待时间最久的线程。公平性是减少线程饥饿(个别线程长期等待,但始终无法获取) 情况发生的一个办法。
如果使用Synchronized,我们无法进行公平性的选择,其永远都是不公平的,这也是主流操作系统调度的选择,在通用场景中,公平性未必有想象中的那么重要,Java默认的调度策略很少导致“饥饿发生”。与此同时如果要保证公平性,则会引入额外开销,自然导致一定的吞吐量下降。所以建议只有当程序确实有公平性需要时,才有必要指定它。
日常编码时,为保证锁释放,每一个lock()动作,都建议立即对应一个try-catch-finally。
ReentrantLock fairLock = new ReentrantLock(true);// 这里是演示创建公平锁,一般情况不需要。
try {
// do something
} fnally {
fairLock.unlock();
}
ReentrantLock 中的重要方法
void lock();// 获取锁,不可被中断,及时当前线程中断,线程一直阻塞,直到得到锁
void lockInterrupted();// 获取锁,优先相应中断,抛出异常
boolean tryLock();// 尝试获取锁,成功true,失败false
boolean tryLock(long time, TimeUnit unit);// 超时返回false
void unlock();// 释放锁
Condition newCondition(); // 返回当前线程的Condition,可多次调用
// 以上为lock接口定义的标准方法
int getQueueLength();// 多少线程在等待
boolean hasQueueLength(); // 是否有线程在等待抢锁
int getHoldCount();// 当前线程是否抢到锁 0:没有
boolean isLock();// 查询是否有线程持有锁
Boolean isFair();// 是否为公平锁
对比
- ReentrantLock相比 Synchronized,因为可以像普通对象一样使用,所以可以利用其提供的各种遍历方法,进行精细的同步操作,甚至实现Synchronized难以表达的用例:
- 带超时的获取锁尝试。
- 可以判断是否有线程,或者某个特定线程在排队等待获取锁。
- 可以相应中断请求。
- 另外ReentrantLock的实现在API、而Synchronized的实现在JVM层
- Synchronized在性能上在低竞争场景中,可能优于ReentrantLock
条件变量 Condition
这里特别强调 java.util.concurrent.condition,如果说ReentrantLock是 Synchronized的替代选择,Condition则是将wait()、notify()、notifyAll()等操作转化为相应的对象,将复杂晦涩的同步操作转变为直观可控的对象行为。
ReentrantLock中Condition使用
void await();// condition线程进入阻塞状态,调用signal()或signalAll()唤醒,允许中断,若有线程中断则抛出异常,必须先获取锁
void awaitUninterruptibly();// 线程进入阻塞状态,可唤醒,不允许中断,如果在阻塞是有线程中断,继续等待唤醒。
long awaitNanos(long nanosTimeout);// 设置阻塞时间,返回值大于0表示被唤醒,其他与await()类似
boolean await(long time,TimeUnit unit);// 被唤醒true
void signal();// 唤醒指定线程
void signalAll();// 唤醒所有线程
条件变量最典型的应用场景就是标准类库中的ArrayBlockingQueue等。
我们参考源码,首先通过再入锁获取条件变量:
/** Condition for waiting takes */
private fnal Condition notEmpty;
/** Condition for waiting puts */
private fnal Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
两个条件变量是使用同一个再入锁创建出来的,然后使用在特定的操作中,如下面的take方法,判断和等待条件满足:
public E take() throws InterruptedException {
fnal ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} fnally {
lock.unlock();
}
}
当队列为空时,试图take的线程的正确行为应该是等待入队发生,而不是直接返回,这是BlockingQueue的语义,使用条件notEmpty就可以优雅的实现这一逻辑。
那么如何保证入队后继续take呢?请看enqueue实现:
private void enqueue(E e) {
// assert lock.isHeldByCurrentThread();
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
fnal Object[] items = this.items;
items[putIndex] = e;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal(); // 通知等待的线程,非空条件已经满足
}
通过signal/await的组合,完成了条件判断和通知等待线程,非常顺畅就完成了状态流转。注意: signal和await的成对调用,不然如果只有await,线程会一直等到被打断。
从性能的角度,Synchronized早期实现比较低效,对比ReentrantLock,大多数场景性能都相差较大。但是在Java6中,进行了非常多的改进,在高竞争的情况下,ReentrantLock还是具有一定的优势的。但是在大多数场景下,无需纠结于性能,还是考虑代码书写结构的便利性和可维护性等。