ReentrantLock实现了Lock接口,并提供了和synchronized相同的互斥性和内存可见性以及可重入的加锁语义。和synchronized相比它再处理锁的不可用性上有更高的灵活性。
下面摘自JDK11文档:
ReentrantLock是一个可重入互斥锁,其基本行为和语义与使用同步方法和语句访问的隐式监视器锁相同,但具有扩展功能。
ReentrantLock由上次成功锁定但尚未解锁的线程所有。当该锁不属于另一个线程时,调用锁的线程将返回并成功获取该锁。如果当前线程已经拥有锁,则该方法将立即返回。这可以使用方法isHeldByCurrentThread和getHoldCount进行检查。
此类的构造函数接受一个可选的公平性参数。当设置为true时,在争用下,锁定有利于授予对等待时间最长的线程的访问权限。否则,此锁不能保证任何特定的访问顺序。使用许多线程访问的公平锁的程序可能显示出比使用默认设置的程序更低的总体吞吐量(即,更慢;通常慢得多),但变化较小。
ReentrantLock 和 synchronized 除了在内存和加锁语义上相同之外,ReentrantLock 还提供了定时的锁等待、可中断的锁等待、公平性、以及实现非块结构的加锁。在性能上ReentrantLock 稍优于synchronized 。但 ReentrantLock 的危险性更高,如果在 finally中忘记释放它,程序仍就能继续运行,但是这是一个大炸弹。
synchronized 是大家都熟悉的一种锁,它简洁紧凑并且在线程转储中能给出在那些调用帧中获得了那些锁,也能检测和识别发生生死锁的线程。与之相比的 ReentrantLock 锁的非块结构决定了它无法确定那些调用帧中获得了那些锁
除非明确需要ReentrantLock 包含的伸缩性,否则还是 synchronized 更香。
ReentrantLock 实现了标准的互斥锁,每次最多只有一个线程持有 ReentrantLock ,从数据的完整性来看互斥是一种强硬的加锁规则。因此很自然的并发就不要想了~
互斥避免了 写/写 冲突,写/读 冲突,但也造成了 读/读冲突,有得有失吧。但是吧,大部分情况下我们都在读读。
所以读写锁一般在一个资源被多个读或者多个写访问的时候使用,这两者不能同时存在。
ReadWriteLock 暴露了两个 Lock 对象,一个是读,一个是写。这两个锁看起来是相互独立的,但是它们只是读-写锁对象的不同视图。
public interface ReadWriteLock {
/**
* 返回用于读取的锁。
*
* @return 用于阅读的锁
*/
Lock readLock();
/**
* 返回用于写的锁。
*
* @return 用于写的锁。
*/
Lock writeLock();
}
ReadWriteLock 可以采用多种不同的实现方式,但同样的它们的性能、优先性、调度、公平性等方面也会不同。
读写锁是一种性能优化措施,在特定的情况下能实现更高的并发性。而在频繁读取的场景下,读写锁可以提高性能。但在其它情况下读写锁比独占锁要差一些,这是因为它们的复杂性更高。
读写锁有多种实现方式,其中都需要考虑以下几点:
说了这么多,我们举个例子看看:
/**
* Auth lhd
* Date 2023/8/3 10:00
* Annotate
*/
public class ReadwriteMape<K,V> {
private final Map<K,V> map;
private final ReadwriteLock lock = new ReentrantReadWriteLock() ;
private final Lock r = lock.readloek();
private final Lock w = lock.writeLock() ;
public ReadWriteMap(MapeK,V map){
this .map = map;
}
public V put(K key, V value) {
W.lock();
try {
return map.put (key, value) ;
}finally {
w.unlock();
}
}
// 对 romove(],putA11(),clear()等方法执行相同的操作
public V get (Object key){
r.lock();
try {
return map.get(key);
}finally {
r.unlock();
}
}
//对其他只读的 Map 方法执行相同的操作
}