Synchronized在大多数情况下已经能够满足需求,但是仍然存在这样的缺点:无法中断正在获取锁的线程,并且在请求失败的情况下需要无限等待
与Synchronized相比,Lock锁更加灵活,当我们需要的锁是可定时的,可轮询的(while trylock),可中断的锁操作的时候,我们应该选择Lock锁,下面是Lock锁的接口声明
public interface Lock {
void lock();
//尝试获取锁,若成功返回true否则返回false
boolean tryLock();
//在尝试获取锁期间可以被中断
void lockInterruptibly() throws InterruptedException;
//尝试在指定时间内获取锁,若成功返回true否则返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
Reentrant提供了与Synchronized一样的可重入加锁的功能.
公平锁与非公平锁
ReentrantLock通过构造函数可以选择创建非公平锁和公平锁。在竞争激烈的情况下,闯入锁比公平锁性能上要好接近100倍.原因在于线程从挂起状态到真正开始运行会有严重的延迟,比如A线程占用了某个锁lock,B请求申请该锁,但是由于lock锁正在被占用于是将B线程挂起,A释放锁时,B线程从挂起状态恢复,这个时候C请求该锁,由于B存在延时所以C得到了该锁.
但是公平锁也有用武之地,当持有锁的时间较长,或者请求锁的平均时间间隔比较长,那么使用公平锁是比较好的
读写锁
在有些应用中读请求远大于写请求,而Reentrant型互斥限制性太强,避免了写/写和读/写同时也避开了读/读,而读请求不会修改共享变量数据结构,所以需要一种锁允许线程并发读数据提高并发性能.下面是读写锁的接口声明.
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
读写锁分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥,这是由JVM控制的,我们只需要记住在读数据的时候加读锁写数据的时候加写锁就可以了。ReentrantReadWriteLock就是读写锁的一种实现,ReentrantReadWriteLock使用两把锁解决问题,一个读锁,一个写锁。
在公平的ReentrantReadWriteLock中,获取锁的条件为:
线程进入读锁的前提条件
1. 没有其他线程申请写锁
2. 没有写请求
线程进入写锁的前提条件
1. 没有其它线程的读锁
2. 没有其它线程的写锁
在非公平的ReentrantReadWriteLock中
线程的访问顺序是不定的,从写者降级为读者是可以的,但从读者升级为写者是不允许的,因为这会造成死锁,比如两个读者同时试图升级到同一个写入锁但是都不释放读锁
我们利用读写锁构建一个支持并发的HashMap数据结构
public class ReadWriteMap<K, V> {
private final Map<K, V> map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public ReadWriteMap() {
map = new HashMap<>();
}
public V put(K key, V value) {
//修改数据结构之前先获取写锁
writeLock.lock();
try {
return map.put(key, value);
} finally {
writeLock.unlock();
}
}
public V get(K key) {
//读取数据之前要先获取读锁
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}
}