读写锁ReentrantReadWriteLock
- 排它锁:Mutex 和 ReentrantLock基本都是排它锁,在同一时刻只允许一个线程进行访问
- 读写锁,同一时刻允许多个读线程访问,但在写线程访问时,所有的读线程和其他线程均被阻塞。
- 读写锁维护了一对锁,通过分离读锁和写锁,使并发性相比一般的排它锁有很大提升
- ReentrantReadWriteLock 特性
- 公平性选择:支持非公平和公平的锁获取方式,吞吐量非公平优于公平
- 重进入:支持重进入
- 锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级为读锁
读写锁接口示例
-
ReadWriteLock进定义了获取读锁和写锁的方法,readLock()和writeLock() 其实现了ReentrantReadWriteLock
-
除了接口方法外还提供了一些便于监控其内部工作状态的方法
- int getReadLockCount:返回当前读锁被获取的次数
- int getReadHoldCount:返回当前现场获取读锁的次数
- boolean isWriteLocked:判断写锁是否被获取
- int getWriteHoldCount():返回当前写锁被获取的次数
-
示例:使用读锁和写锁来保证cache是线程安全的
-
import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; 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(); } } 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(); } } }
读写锁实现分析
-
读写状态设计:就是同步器的同步状态
- 同步状态表示锁被一个线程重复获取的次数,读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程状态
- 整型变量,需要按位切割使用这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。
- 根据状态划分:S不等于0时,当写状态等于0时,则读状态大于0,即读锁已被获取
-
写锁的获取与释放
- 写锁:tryAcquire支持重进入的排它锁,
- 如果当前线程已经获取了写锁,则增加写状态
- 如果当前线程在获取写锁时,读锁已经被获取或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
- 该方法除了重入条件,增加了一个读锁是否存在的判断,如果存在,则写锁不能获取
- 写锁释放:每次释放均减少写状态,当写状态为0时,表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。
- 写锁:tryAcquire支持重进入的排它锁,
-
读锁的获取与释放
- 读锁获取tryAcquireShared:支持重进入的共享锁,能够被多个线程同时获取,在没有其他写线程访问时,读锁总会被成功的获取,只是增加读状态。
- 如果当前线程已经获取了读锁,增加读状态
- 如果当时在获取读锁时,写锁已被其他线程获取,则进入等待状态。
- 新增功能,例如getReadHoldCount,作用是返回当前线程获取读锁的次数
- 读状态是所有线程获取读锁次数的总和,每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护,使获取读锁的实现变得复杂
- 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态
- 每次释放均减少读状态,减少的值是1<<16
- 读锁获取tryAcquireShared:支持重进入的共享锁,能够被多个线程同时获取,在没有其他写线程访问时,读锁总会被成功的获取,只是增加读状态。
-
锁降级:写锁降级为读锁,是指把持住(当前拥有的)写锁,再次获取到读锁,所有释放(先前拥有的)写锁的过程
-
为了保证数据可见性,锁降级中读锁的获取是必要的, ReentrantReadWriteLock不支持锁升级
-
示例:当数据发生变更后,update变量被设置为false,此时所有访问processData方法的线程都能感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁he写锁的lock()方法上,当前线程获取写锁完成数据准备之后,在获取读锁,随后释放写锁,完成锁降级
-
public void processData(){ readLock.lock(); if (!update){ //必须先释放读锁 readLock.unlock; //锁降级从写锁获取到开始 writeLock.lock(); try { if(!update){ //准备数据的流程 update = true; } readLock.lock(); }finally { writeLock.unlock(); } //锁降级完成,写锁降级为读锁 } try { //使用数据 }finally { readLock.lock(); } }
-