【java并发工具类-互斥】ReadWriteLock(读写锁)


开发中经常碰到这样一种场景: 读多写少 ,比如缓存,缓存之所以可以提升性能,一个重要的条件就是缓存的数据一定是读多写少的,缓存中的数据基本不会发生变化(写少),经常性地读缓存的数据(读多)。

针对这种场景,Java SDK并发包提供了读写锁——ReadWriteLock,和比读写锁更快的锁——StampedLock。

1.ReadWriteLock(读写锁)

1.2 什么是读写锁?

读写锁遵循下面这三条规则:

  • 允许多个线程同时读共享变量;
  • 只允许一个线程进行写共享变量;
  • 如果一个线程正在执行写操作,此时禁止其他线程读共享变量。

读写锁和互斥锁的区别是:读写锁允许多个线程进行读操作。
相同点是:读写锁在进行写操作时,是不允许其他线程进行读操作或者写操作。

1.3 使用读写锁快速实现一个缓存?

实现缓存这个工具类,我们提供两个方法,读缓存(get()方法),写缓存(put()方法)。下面是我们的代码:

class Cache<K,V> {
 final Map<K, V> m=new HashMap<>();//final域禁止写final域引用对象以及第一次写引用对象成员域到构造函数外,之前文章提到过
 final ReadWriteLock rwl =new ReentrantReadWriteLock();
 final Lock r = rwl.readLock();  // 读锁
 final Lock w = rwl.writeLock(); // 写锁
  V get(K key) { // 读缓存
    r.lock();
    try { return m.get(key); }
    finally { r.unlock(); }
  }
  V put(K key, V value) {  // 写缓存
    w.lock();
    try { return m.put(key, v); }
    finally { w.unlock(); }
  }
}

下面我们来真正实现缓存:

  1. 实现缓存的话,首先也需要解决缓存的初始化问题。两种方式:1.一次性加载数据 2.按需加载数据
    在这里插入图片描述
    下面实现缓存的按需加载:在查询缓存中数据,如果不存在就从数据库中查询到缓存中(写缓存需要再次判断是否被其他线程写了),
class Cache<K,V> {
  final Map<K, V> m =new HashMap<>();
  final ReadWriteLock rwl = new ReentrantReadWriteLock();
  final Lock r = rwl.readLock();
  final Lock w = rwl.writeLock(); 
  V get(K key) {
    V v = null;
    r.lock(); //读缓存         ①
    try {
      v = m.get(key);} finally{
      r.unlock();}
    //缓存中存在,返回
    if(v != null) {return v;
    }  
    //缓存中不存在,查询数据库
    w.lock();try {
      //再次验证
      //其他线程可能已经查询过数据库
      v = m.get(key);if(v == null){//查询数据库
        v=省略代码无数
        m.put(key, v);
      }
    } finally{
      w.unlock();
    }
    return v; 
  }
}
  1. 除此之外,也需要解决缓存数据和源头数据的同步问题,两者需要保证一致性。
    解决方法:1.超时机制:当缓存数据超过时效,就让这条数据在缓存中失效,等待再次访问把源数据加载到缓存中。
    2.或者在源头数据发生修改,快速反馈给缓存,发生修改了,就将最新的数据存到缓存中。
    3.数据库和缓存的双写方案。
1.4 读写锁的升级和降级
1.4.1 ReadWriteLock 是不允许升级的!

先来看一个实例代码:

r.lock();//读缓存
try {
  v = m.get(key);if (v == null) {
    w.lock();
    try {   
      //省略详细代码 //再次验证并更新缓存
    } finally{
      w.unlock();
    }
  }
} finally{
  r.unlock();}

上面中 1.先获取读锁,之后又获取写锁,这叫锁的升级,但是**ReadWriteLock 并不支持这种升级!**上面的代码中,读锁还没有被释放,然后又要获得写锁,会导致写锁永久等待,然后导致线程被阻塞,服务器可能表现cpu利用率低。

1.4.1 ReadWriteLock 允许降级

下面是实现缓存数据按需加载的另外一种方式,直接上代码:

class CachedData {
  Object data;
  volatile boolean cacheValid;
  final ReadWriteLock rwl =new ReentrantReadWriteLock();
  final Lock r = rwl.readLock(); // 读锁 
  final Lock w = rwl.writeLock();  //写锁 
  void processCachedData() {
    r.lock();   // 获取读锁
    if (!cacheValid) { //先判读缓存中是否存在该数据,如果存在跳过if下面的方法,直接use(data)然后释放读锁。
    //否则需要写入缓存数据,获取写锁进行写操作,因为不允许锁的升级,所以需要先释放读锁,获取写锁,把数据写入缓存
      r.unlock(); // 释放读锁,因为不允许读锁的升级     
      w.lock(); // 获取写锁
      try {
        // 获取写锁之前有可能其他线程获取写锁写入了,所以再次检查状态  
        if (!cacheValid) {//如果没有写入缓存,就写入
          data = ...
          cacheValid = true;
        }
        // 因为下面需要释放读锁,我们需要再获得读锁,让其有东西释放,同时允许锁降级,所以直接获得读锁。
        r.lock();  // 释放写锁前,降级为读锁,这个锁也需要释放
      } finally {
        // 释放写锁
        w.unlock(); 
      }
    }
    // 此处仍然持有读锁
    try {use(data);} 
    finally {r.unlock();}//释放读锁
  }
}
1.5 读写锁的注意事项
  1. ReadWriteLock 是一个接口,它的实现类是ReentrantReadWriteLock,通过名字你可以看出来,它是可重入的。
  2. 同时它获取的读锁和写锁实现了Lock接口,所以除了支持lock()方法外,也支持非阻塞式获取锁tryLock(),lockInterruptibly()等方法。
  3. 读写锁类似于 ReentrantLock,也支持公平模式和非公平模式。
  4. 注意:它的写锁支持条件变量,但是读锁是不支持条件变量的,读锁调用newCondition()会抛出异常。

参考:极客时间
更多:邓新

扫描二维码关注公众号,回复: 10816334 查看本文章
发布了34 篇原创文章 · 获赞 0 · 访问量 1089

猜你喜欢

转载自blog.csdn.net/qq_42634696/article/details/105111777