前言
相信磁盘缓存在绝大部分的 App 上都有应用, 相对于数据库缓存来说, 可以不要注重于缓存的管理, 比较开放和随意.
再加上 jakewharton 早年间发布的 disklrucache 框架, 让我们使用磁盘缓存更加简单, 效率上和数据库缓存也拉进了一步, 以后有时间我在加上 disklrucache 的缓存解读.
但是在多线程的环境下, 对同一份数据进行读写, 会涉及到线程安全的问题. 比如在一个线程读取数据的时候, 另外一个线程在写数据, 而导致前后数据的不一致性; 一个线程在写数据的时候, 另一个线程也在写, 同样也会导致线程前后看到的数据的不一致性. 更严重的是一个线程在写的时候, 另一个线程在读. 这里的数据不一致是对于文件来说的, 当文件里的数据存储的 JSON 时, 残缺的数据或者不完整的数据无法生成对象, 判断没有写好甚至是报错闪退.
常见解决方案
使用 Synchronized 同步锁保护线程安全, 但是 Synchronized 存在明显的一个性能问题就是读与读之间互斥, 也就是说两个线程的读操作是顺序执行的 下面给大家看下代码方便理解
- public static void main(String[] args) {
- new Thread(new Runnable() {
- @Override
- public void run() {
- read(Thread.currentThread());
- }
- }).start();
- new Thread(new Runnable() {
- @Override
- public void run() {
- read(Thread.currentThread());
- }
- }).start();
- }
- public synchronized static void read(Thread thread){
- System.out.println("开始运行时间:"+System.currentTimeMillis());
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("结束运行时间:"+System.currentTimeMillis());
- }
我们来看一下运行结果, 结论两个两个线程的读操作是顺序执行的, 如果读的次数多这个太影响性能了
image
思考
最佳的方案通俗的来讲应该是, 可以很多人同时读, 但不能同时写, 有人在写的时候不能同时读也不能同时写, 官方说法是读和读互不影响, 读和写互斥, 写和写互斥, 好了接下来就是介绍今天的主角 ReadWriteLock 读写锁
ReadWriteLock 介绍
1.1 ReadWriteLock 的位置
ReadWriteLock 是 Java 自带的 所处位置 java.util.concurrent.locks, 属于 java 并发方案中的一种
1.2 ReadWriteLock 是一个接口, 主要有两个方法, 如下
- public interface ReadWriteLock {
- /**
- * Returns the lock used for reading.
- *
- * @return the lock used for reading
- */
- Lock readLock();
- /**
- * Returns the lock used for writing.
- *
- * @return the lock used for writing
- */
- Lock writeLock();
- }
既然只是接口, 那我们真正要用的是实现了该接口的类 ReentrantReadWriteLock 可重入读写锁
1.3 可重人
可重入锁, 就是说一个线程在获取某个锁后, 还可以继续获取该锁, 即允许一个线程多次获取同一个锁. 通俗的来讲就是支持在同一个线程里面对多个文件进行读写操作, 都可以获取同一个锁, 但是获取多少锁就要回收多少锁, 下面给个例子方便理解
- public static void main(String[] args) {
- final ReadWriteLock lock = new ReentrantReadWriteLock();
- lock.writeLock().lock();
- lock.writeLock().lock();
- new Thread(new Runnable() {
- @Override
- public void run() {
- lock.writeLock().lock();
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("子线程运行");
- lock.writeLock().unlock();
- }
- }).start();
- System.out.println("主线程运行");
- lock.writeLock().unlock();
- // lock.writeLock().unlock(); 获取两次锁, 只释放一次锁
- }
运行结果
image
注意: 因为主线程 2 次获取了锁, 但是却只释放 1 次锁, 造成死锁, 导致新线程永远也不能获取锁. 一个线程获取多少次锁, 就必须释放多少次锁
1.4 获取锁顺序
非公平模式 (默认)
当以非公平初始化时, 读锁和写锁的获取的顺序是不确定的. 非公平锁主张竞争获取, 可能会延缓一个或多个读或写线程, 但是会比公平锁有更高的吞吐量.
公平模式
当以公平模式初始化时, 线程将会以队列的顺序获取锁. 当当前线程释放锁后, 等待时间最长的写锁线程就会被分配写锁; 或者有一组读线程组等待时间比写线程长, 那么这组读线程组将会被分配读锁.
源码如下
- public ReentrantReadWriteLock() {
- this(false);
- }
- /**
- * Creates a new {@code ReentrantReadWriteLock} with
- * the given fairness policy.
- *
- * @param fair {@code true} if this lock should use a fair ordering policy
- */
- public ReentrantReadWriteLock(boolean fair) {
- sync = fair ? new FairSync() : new NonfairSync();
- readerLock = new ReadLock(this);
- writerLock = new WriteLock(this);
- }
1.5 锁升级和锁降级
锁降级: 从写锁变成读锁;
锁升级: 从读锁变成写锁.
ReentrantReadWriteLock 只支持锁降级
建议尽量不要使用锁降级操作, 获取什么锁就回收什么锁, 同一线程尽量不要使用两种锁, 最为安全, 除非有特殊操作则需注意
2 磁盘缓存最佳设计
提供抽象类 BaseCache 的源码, 具体实现大家可以通过自己的实际情况去拓展
- public abstract class BaseCache {
- private final ReadWriteLock mLock = new ReentrantReadWriteLock();
- /**
- * 读取缓存
- *
- * @param key 缓存 key
- * @param existTime 缓存时间
- */
- final <T> T load(Type type, String key, long existTime) {
- //1. 先检查 key
- Utils.checkNotNull(key, "key == null");
- //2. 判断 key 是否存在, key 不存在去读缓存没意义
- if (!containsKey(key)) {
- return null;
- }
- //3. 判断是否过期, 过期自动清理
- if (isExpiry(key, existTime)) {
- remove(key);
- return null;
- }
- //4. 开始真正的读取缓存
- mLock.readLock().lock();
- try {
- // 读取缓存
- return doLoad(type, key);
- } finally {
- mLock.readLock().unlock();
- }
- }
- /**
- * 保存缓存
- *
- * @param key 缓存 key
- * @param value 缓存内容
- * @return
- */
- final <T> boolean save(String key, T value) {
- //1. 先检查 key
- Utils.checkNotNull(key, "key == null");
- //2. 如果要保存的值为空, 则删除
- if (value == null) {
- return remove(key);
- }
- //3. 写入缓存
- boolean status = false;
- mLock.writeLock().lock();
- try {
- status = doSave(key, value);
- } finally {
- mLock.writeLock().unlock();
- }
- return status;
- }
- /**
- * 删除缓存
- */
- final boolean remove(String key) {
- mLock.writeLock().lock();
- try {
- return doRemove(key);
- } finally {
- mLock.writeLock().unlock();
- }
- }
- /**
- * 获取缓存大小
- * @return
- */
- long size() {
- return getSize();
- }
- /**
- * 清空缓存
- */
- final boolean clear() {
- mLock.writeLock().lock();
- try {
- return doClear();
- } finally {
- mLock.writeLock().unlock();
- }
- }
- /**
- * 是否包含 加 final 是让子类不能被重写, 只能使用 doContainsKey
- * 这里加了锁处理, 操作安全.<br>
- *
- * @param key 缓存 key
- * @return 是否有缓存
- */
- public final boolean containsKey(String key) {
- mLock.readLock().lock();
- try {
- return doContainsKey(key);
- } finally {
- mLock.readLock().unlock();
- }
- }
- /**
- * 是否包含 采用 protected 修饰符 被子类修改
- */
- protected abstract boolean doContainsKey(String key);
- /**
- * 是否过期
- */
- protected abstract boolean isExpiry(String key, long existTime);
- /**
- * 读取缓存
- */
- protected abstract <T> T doLoad(Type type, String key);
- /**
- * 保存
- */
- protected abstract <T> boolean doSave(String key, T value);
- /**
- * 删除缓存
- */
- protected abstract boolean doRemove(String key);
- /**
- * 清空缓存
- */
- protected abstract boolean doClear();
- /**
- * 获取缓存大小
- *
- * @return
- */
- protected abstract long getSize();
- }
来源: http://www.jianshu.com/p/4c925ebf3d34