写点什么

读写锁

用户头像
Geek_571bdf
关注
发布于: 2021 年 05 月 09 日

1. ReadWriteLock,读写锁。包括读锁和写锁。规则是这样的:

①允许多个线程同时读共享变量,即 读锁与读锁之间是不互斥的;

②同一时间只允许一个线程写共享变量,即,读锁与写锁、写锁与写锁之间是互斥的;也就是说,如果有线程在写共享变量,此时禁止其它线程读共享变量。

 

2. ReadWriteLock 同样也支持公平锁和非公平锁模式,但需要注意的是,只有写锁支持条件变量,读锁不支持条件变量。// 事实上也不需要。

 

3. 实现一个“按需加载”的缓存。——使用缓存首先要解决数据初始化问题,如果数据量不大,那么可以在应用启动时一次性将数据从数据源(如 MySQL 数据库)查询出来,然后依次调用 put 方法添加至缓存。如果数据量很大,那么就只能使用按需加载,也叫懒加载。


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();    // ReadWriteLock不支持锁升级,因此需要先释放读锁,然后才能再进一步获取写锁    }    if(v != null) {         return v;    }      // 如果数据在缓存中不存在,查询数据库    w.lock(); // 上写锁   ②    try {      //再次验证,因为在这个过程中,其它线程可能已经查询过数据库了      v = m.get(key);       if(v == null){          //查询数据库        m.put(key, v);      }    } finally{      w.unlock();    }    return v;   }}
复制代码

注意,在获取写锁后,需要再次验证。因为在高并发场景下,可能会有多个线程竞争写锁。假设现在缓存是空的,此时,T1、T2、T3 同时调用 get()方法,且方法参数 key 都相同。它们会同时执行到代码②处,但只有一个线程能获取写锁。假设是 T1,那么在 T1 执行完之后,缓存中是已经有我们需要的数据了的。而如果没有再次验证,那么 T2 和 T3 会重复查询数据库。因此,通过这样的再次验证的方式,能够避免高并发场景下重复查询数据的问题。


4. 缓存还需要解决 缓存数据与源头数据的同步问题。

  • 一种方案是 超时机制。我们为加载进缓存的数据都设置一个时效,当缓存中的数据超过时效之后,那么就表示该数据失效了,需要重新从源头将数据加载进缓存中。

  • 另一种方案是,在源头数据发生变化时,快速反馈给缓存。当然,此方案就依赖具体的场景了。例如 MySQL 作为数据源头,可以通过近实时地解析 binlog 来识别数据是否发生了变化,如果发生了变化就将最新的数据推送给缓存。

  • 此外,还可以采取数据库和缓存的双写方案。

 

具体采用哪种方案,还是要看应用场景。

 

5. 锁升级与锁降级

ReadWriteLock 只支持锁降级,不支持锁升级。

所谓锁升级,指的是线程在持有读锁、且不释放读锁的情况下,进一步申请写锁。而所谓锁降级,指的是线程在持有写锁、且不释放写锁的情况下,进一步申请读锁。——注意,所谓升级、降级不是合并,进行锁升级后,读锁和写锁仍然要分别释放。

 

锁升级的应用,比如上边的缓存实现,当缓存中没有我们想要的数据时,我们需要进一步使用写锁,来查询数据库并写缓存。

锁降级的应用:当持有写锁获取到数据之后,我们后续需要对该数据继续使用(非写操作),那么释放写锁、然后持有读锁对该数据进行后续的使用,这显得十分合理。反之,如果仅仅是简单的把数据返回,就不要需要锁降级了。


用户头像

Geek_571bdf

关注

还未添加个人签名 2019.06.13 加入

还未添加个人简介

评论

发布
暂无评论
读写锁