显式锁

Lock与ReentrantLock

1
2
3
4
5
6
7
8
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
void unlock;
Condition newCondition();
}

与内置加锁机制不同,Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的。

Lock的标准使用形式:

1
2
3
4
5
6
7
8
9
Lock lock = new ReentrantLock();
...
lock.lock();
try {
// 更新对象状态
// 捕获异常,并在必要时恢复不变性条件
} finally {
lock.unlock();
}

必须在finally块中释放锁,否则相当于启动了一个定时炸弹。

轮询锁与定时锁

与无条件的锁获取模式相比,可定时的、可轮询的锁获取模式具有更完善的错误恢复机制,来避免死锁。

如果不能获得所有需要的锁,那么可以使用定时的或可轮询的锁获取方式,从而使你重新获得控制权,它会释放已经获得的锁,然后重新尝试获取所有锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean transferMoney(Account from, Account to
DollarAmount amount, long timeout, Timeunit unit) {
while(true) {
if (from.lock.tryLock()) {
try {
if (to.lock.tryLock()) {
try {
return transfer(from, to, amount);
} finally {
to.lock.unlock();
}
}
} finally {
from.lock.unlock();
}
}
if (retry too many times) {
return false;
}
Thread.sleep(random time);
}
}

在实现具有时间限制的操作时,定时锁非常有用。当时用内置锁时,在开始请求锁后,这个操作将无法取消。可使用tryLock(timeout, timeunit)方法来实现。

可中断的锁获取操作

请求内置锁时,无法响应中断。这些不可中断的阻塞机制,将使得实现可取消任务变得复杂。lockInterruptibly()方法能够在获得锁的同时保持对中断的响应。

非块结构的加锁

连锁式加锁Hand-Over-Hand Locking
锁耦合Lock Coupling

性能考虑因素

性能是个不断变化的指标,如果昨天的测试基准中发现X比Y快,那么在今天就可能已经过时了。

公平性

非公平的锁允许插队:当一个线程请求非公平锁时,如何在发出请求的同时该锁的状态可用,那么这个线程将跳过队列中所有的等待线程,并获得这个锁。

非公平锁的性能高于公平锁。公平性将由于挂起线程和恢复线程时存在的开销而极大降低性能,实际情况下,统计上的公平性保证————确保被阻塞的线程能最终获得锁,已经够用了,并且开销小得多。

在持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁。这种情况下,插队带来的吞吐量提升则可能不会出现。

与默认ReentrantLock一样,内置锁不会提供确定的公平性保证,大多数情况下,实现统计上的公平性保证就已经足够了。

在synchronized和ReentrantLock之间进行选择

在内置锁无法满足需求的情况下,ReentrantLock可作为一种高级工具,如可定时的、可轮询的、可中断的锁获取操作,公平队列,以及非块结构的锁,才是用ReentrantLock。否则还是优先使用synchronized。

读写锁

如果能够放宽互斥的加锁策略,允许多个执行读操作的线程同时访问数据,那么将提升程序的性能。

ReentrantReadWriteLock在构造时,可选择非公平(默认)还是公平锁。写线程降级为读线程是可以的,但从读线程升级为写线程则不可以(会导致死锁)。

当锁的持有时间较长,且大部分操作都不会修改被守护的资源时,那么读写锁能提供并发性。