5.3 重入锁
重入锁(ReentrantLock)就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。
5.3.1 实现重进入
- 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
- 锁的最终释放:线程重复N次获取锁,随后需要第N次释放锁后,才算释放,这期间需要维护一个计数器。
5.3.2 公平与非公平获取锁的区别
公平锁保证了锁的获取按照FIFO的原则,而代价是进行大量的线程切换。非公平性锁虽然造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。
5.4 读写锁
读写锁允许在同一个时刻多个线程同时访问。
J.U.C中提供读写锁的实现是ReentrantReadWriteLock,它的特性如下:
特性 | 说明 |
---|---|
公平性选择 | 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平 |
重进入 | 支持重进入 |
锁降级 | 遵循获取写锁、获取读锁再获取写锁的次序,写锁可以降级成为读锁 |
5.4.1 读写锁的接口与示例
public class Cache {
static Map<String, Object> map = new HashMap<>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
//获取一个key对应的value
public static final Object get(String key){
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
//设置key对应的value,并返回旧的value
public static final Object put(String key,Object value){
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
//清空所有的内容
public static final void clear(){
w.lock();
try {
map.clear();
} finally {
w.unlock();
}
}
}
上述示例中,Cache组合一个非线程安全的hashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。
在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。
写操作put(String key,Object value)方法和clear()方法,在更新HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放后,其他读操作才可以继续。
5.4.2 读写锁的实现分析
- 读写状态的设计
读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态
在一个整型变量上维护多种状态,需要“按位切割使用”,高16位表示读,低16位表示写。
读写锁是如何迅速确定读和写各自的状态?通过位运算。
假设当前同步状态值为S,写状态等于 S & 0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。
当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000
推论:S不等于0时,当写状态(S & 0x0000FFFF)等于0时,则读状态(S >>>16)大于0,即读锁已被获取。
- 写锁的获取与释放
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者线程不是已经获取写锁的线程,则当前线程进入等待状态。 - 读锁的获取与释放
读锁是一个支持重进入的共享锁。 锁降级
锁降级指的是写锁降级成为读锁。锁降级指的是把持住(当前拥有的)写锁,再获取到读锁,随后释放先前拥有的写锁的过程。
示例:数据不变化时,多个线程可以并发的进行数据处理,当数据变化时,如果当前线程感知了数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。public void processData(){ readLock.lock(); if(!update){ //先释放读锁 readLock.unlock(); // 再获取写锁,视为降级开始 writeLock.lock(); try { if(!update){ // 准备数据的流程(略) update = true; } readLock.lock(); } finally { writeLock.unlock(); } // 锁降级完成,写锁降级为读锁 } try { //使用数据的流程(略) } finally { readLock.unlock(); } }
update的变量用volatile修饰,保证线程可见。