读写锁
1. ReadWriteLock,读写锁。包括读锁和写锁。规则是这样的:
①允许多个线程同时读共享变量,即 读锁与读锁之间是不互斥的;
②同一时间只允许一个线程写共享变量,即,读锁与写锁、写锁与写锁之间是互斥的;也就是说,如果有线程在写共享变量,此时禁止其它线程读共享变量。
2. ReadWriteLock 同样也支持公平锁和非公平锁模式,但需要注意的是,只有写锁支持条件变量,读锁不支持条件变量。// 事实上也不需要。
3. 实现一个“按需加载”的缓存。——使用缓存首先要解决数据初始化问题,如果数据量不大,那么可以在应用启动时一次性将数据从数据源(如 MySQL 数据库)查询出来,然后依次调用 put 方法添加至缓存。如果数据量很大,那么就只能使用按需加载,也叫懒加载。
注意,在获取写锁后,需要再次验证。因为在高并发场景下,可能会有多个线程竞争写锁。假设现在缓存是空的,此时,T1、T2、T3 同时调用 get()方法,且方法参数 key 都相同。它们会同时执行到代码②处,但只有一个线程能获取写锁。假设是 T1,那么在 T1 执行完之后,缓存中是已经有我们需要的数据了的。而如果没有再次验证,那么 T2 和 T3 会重复查询数据库。因此,通过这样的再次验证的方式,能够避免高并发场景下重复查询数据的问题。
4. 缓存还需要解决 缓存数据与源头数据的同步问题。
一种方案是 超时机制。我们为加载进缓存的数据都设置一个时效,当缓存中的数据超过时效之后,那么就表示该数据失效了,需要重新从源头将数据加载进缓存中。
另一种方案是,在源头数据发生变化时,快速反馈给缓存。当然,此方案就依赖具体的场景了。例如 MySQL 作为数据源头,可以通过近实时地解析 binlog 来识别数据是否发生了变化,如果发生了变化就将最新的数据推送给缓存。
此外,还可以采取数据库和缓存的双写方案。
具体采用哪种方案,还是要看应用场景。
5. 锁升级与锁降级
ReadWriteLock 只支持锁降级,不支持锁升级。
所谓锁升级,指的是线程在持有读锁、且不释放读锁的情况下,进一步申请写锁。而所谓锁降级,指的是线程在持有写锁、且不释放写锁的情况下,进一步申请读锁。——注意,所谓升级、降级不是合并,进行锁升级后,读锁和写锁仍然要分别释放。
锁升级的应用,比如上边的缓存实现,当缓存中没有我们想要的数据时,我们需要进一步使用写锁,来查询数据库并写缓存。
锁降级的应用:当持有写锁获取到数据之后,我们后续需要对该数据继续使用(非写操作),那么释放写锁、然后持有读锁对该数据进行后续的使用,这显得十分合理。反之,如果仅仅是简单的把数据返回,就不要需要锁降级了。
评论