我们之前常用到的ReentrantLock和Synchorinized都是排他锁,意思都是同一时间只能有一个线程能够获取锁。
1 概念
读写锁:表示同一时刻允许多个读线程并发访问,但是在写线程访问时,所有的读线程和写线程都被阻塞。和数据库的共享锁类似。
2.并发包中读写锁的的实现类ReentrantReadWriteLock
2.1. 读写锁的实现ReentrantReadWriteLock,它是继承ReadWriteLock接口而不是Lock接口。
ReentrantReadWriteLock特点:
2.2 ReentrantReadWriteLock有两个静态内部类 ,它是继承Lock接口的,主要用这两个是实现读写锁的实现
public static class ReadLock implements Lock, java.io.Serializable
public static class WriteLock implements Lock, java.io.Serializable
2.3 ReentrantReadWriteLock 还提供一系列方法来展示工作状态的方法,当然不止这些,具体的可以参考Java API.
2.4 例子:
package com.wusu.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();
public static final Object get(String key){
r.lock();
try {
return map.get(key); //获取key对应的value值
}finally {
r.unlock();
}
}
public static final Object set(String key,Object value){
w.lock();
try {
return map.put(key,value); //设置key对应的value
}finally {
w.unlock();
}
}
//清空map
public static final void clear(){
w.lock();
try {
map.clear(); //设置key对应的value
}finally {
w.unlock();
}
}
}
如上所示:HashMap是线程不安全的,但是使用读写锁来保证Cache是线程安全的。这里的并发访问读,都可以获取读锁,除了写访问;对于访问写,必须获取写锁,使得其他读和写阻塞。这里用读写锁提高读操作的并发性,也保证了每次写操作对所有的读写操作的可见性。
3.读写锁的实现分析
3.1 读写状态的设计
读写锁依赖同步器(AQS)来实现,而读写状态就是其同步器的同步状态,这里的读写锁需要在同步状态上维护多个读线程和一个写线程。实现是通过把变量(32位 -int类型)“按位切割使用” 高16位表示读,低16位表示写。
3.2 写锁的获取和释放
写锁writeLock 是支持重入锁的,表示当前线程获取了写锁,还可以增加写锁。如果当前线程在获取写锁时,读锁已经被获取了或者当前线程不是已经获取了写锁,那么就会处于等待状态。具体可以看源码的(tryAcquire方法)
另外读锁如果存在,那么写锁就不能够被获取,原因是因为读写锁要确保写锁对读锁可见。
3.3读锁的获取和释放
读锁也是支持重进入的共享锁,能够被多个线程同时获取,可以参考源码方法(tryAcquireShared)
3.4 锁降级
锁降级指的是写锁降级成为读锁。解释:当前线程把持住写锁再获取到读锁,随后释放先前拥有的写锁的过程(如果当前进程获取到了写锁是允许继续获取读锁的)
为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此时有另一个线程T获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新,如果当前线程获取读锁,则线程T会被阻塞,直到当前线程使用数据并施放读锁之后,线程T才能获取写锁进行数据更新。
官网例子:
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
参考网上给的实例网址:https://blog.csdn.net/u010512429/article/details/80011239
用下面的代码来解释,若在第2行和第6行上并不加读锁的获取和释放(即//2和//6被注释掉),当前线程完成第1行的put操作后,进行第3行的写锁释放操作,若此时有另一个线程获得写锁并进行修改(即进行第1行操作),那么当前获得读锁的线程无法感知,在进行第4行和第5行操作时就会发生线程安全性问题。若加上第2行和第6行,由于读锁和写锁之间是互斥的,当写锁释放的时候,由于第2行读锁的存在,新的线程并不能获得写锁,此时就保证了线程安全性。
例子:
private Map<String, Object> map = new HashMap<>();
private ReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock r = rwl.readLock();
private Lock w = rwl.writeLock();
private volatile boolean isUpdate;
public void readWrite() {
r.lock(); // 为了保证isUpdate能够拿到最新的值
if (!isUpdate) {
r.unlock();
w.lock();
map.put("xxx", "xxx");//1
r.lock();//2
w.unlock();//3
}
Object obj = map.get("xxx");//4
System.out.println(obj);//5
r.unlock();//6
}