前言
相信磁盤(pán)緩存在絕大部分的app上都有應(yīng)用栏账,相對(duì)于數(shù)據(jù)庫(kù)緩存來(lái)說(shuō),可以不要注重于緩存的管理栈源,比較開(kāi)放和隨意挡爵。
再加上jakewharton早年間發(fā)布的disklrucache框架,讓我們使用磁盤(pán)緩存更加簡(jiǎn)單甚垦,效率上和數(shù)據(jù)庫(kù)緩存也拉進(jìn)了一步茶鹃,以后有時(shí)間我在加上disklrucache的緩存解讀。
但是在多線程的環(huán)境下艰亮,對(duì)同一份數(shù)據(jù)進(jìn)行讀寫(xiě)前计,會(huì)涉及到線程安全的問(wèn)題。比如在一個(gè)線程讀取數(shù)據(jù)的時(shí)候垃杖,另外一個(gè)線程在寫(xiě)數(shù)據(jù)男杈,而導(dǎo)致前后數(shù)據(jù)的不一致性;一個(gè)線程在寫(xiě)數(shù)據(jù)的時(shí)候调俘,另一個(gè)線程也在寫(xiě)伶棒,同樣也會(huì)導(dǎo)致線程前后看到的數(shù)據(jù)的不一致性。更嚴(yán)重的是一個(gè)線程在寫(xiě)的時(shí)候彩库,另一個(gè)線程在讀肤无。這里的數(shù)據(jù)不一致是對(duì)于文件來(lái)說(shuō)的,當(dāng)文件里的數(shù)據(jù)存儲(chǔ)的json時(shí)骇钦,殘缺的數(shù)據(jù)或者不完整的數(shù)據(jù)無(wú)法生成對(duì)象宛渐,判斷沒(méi)有寫(xiě)好甚至是報(bào)錯(cuò)閃退。
常見(jiàn)解決方案
使用Synchronized同步鎖保護(hù)線程安全眯搭,但是Synchronized存在明顯的一個(gè)性能問(wèn)題就是讀與讀之間互斥窥翩,也就是說(shuō)兩個(gè)線程的讀操作是順序執(zhí)行的 下面給大家看下代碼方便理解
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("開(kāi)始運(yùn)行時(shí)間:"+System.currentTimeMillis());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("結(jié)束運(yùn)行時(shí)間:"+System.currentTimeMillis());
}
我們來(lái)看一下運(yùn)行結(jié)果,結(jié)論兩個(gè)兩個(gè)線程的讀操作是順序執(zhí)行的,如果讀的次數(shù)多這個(gè)太影響性能了
思考
最佳的方案通俗的來(lái)講應(yīng)該是鳞仙,可以很多人同時(shí)讀寇蚊,但不能同時(shí)寫(xiě),有人在寫(xiě)的時(shí)候不能同時(shí)讀也不能同時(shí)寫(xiě)棍好,官方說(shuō)法是讀和讀互不影響仗岸,讀和寫(xiě)互斥,寫(xiě)和寫(xiě)互斥借笙,好了接下來(lái)就是介紹今天的主角ReadWriteLock 讀寫(xiě)鎖
ReadWriteLock介紹
1.1 ReadWriteLock的位置
ReadWriteLock是Java自帶的 所處位置 java.util.concurrent.locks扒怖,屬于java并發(fā)方案中的一種
1.2 ReadWriteLock是一個(gè)接口,主要有兩個(gè)方法业稼,如下
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();
}
既然只是接口盗痒,那我們真正要用的是實(shí)現(xiàn)了該接口的類(lèi) ReentrantReadWriteLock 可重入讀寫(xiě)鎖
1.3可重人
可重入鎖,就是說(shuō)一個(gè)線程在獲取某個(gè)鎖后盼忌,還可以繼續(xù)獲取該鎖积糯,即允許一個(gè)線程多次獲取同一個(gè)鎖掂墓。通俗的來(lái)講就是支持在同一個(gè)線程里面對(duì)多個(gè)文件進(jìn)行讀寫(xiě)操作,都可以獲取同一個(gè)鎖看成,但是獲取多少鎖就要回收多少鎖君编,下面給個(gè)例子方便理解
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("子線程運(yùn)行");
lock.writeLock().unlock();
}
}).start();
System.out.println("主線程運(yùn)行");
lock.writeLock().unlock();
// lock.writeLock().unlock(); 獲取兩次鎖,只釋放一次鎖
}
運(yùn)行結(jié)果
注意:因?yàn)橹骶€程2次獲取了鎖川慌,但是卻只釋放1次鎖吃嘿,造成死鎖,導(dǎo)致新線程永遠(yuǎn)也不能獲取鎖梦重。一個(gè)線程獲取多少次鎖兑燥,就必須釋放多少次鎖
1.4 獲取鎖順序
-
非公平模式(默認(rèn))
當(dāng)以非公平初始化時(shí),讀鎖和寫(xiě)鎖的獲取的順序是不確定的琴拧。非公平鎖主張競(jìng)爭(zhēng)獲取降瞳,可能會(huì)延緩一個(gè)或多個(gè)讀或?qū)懢€程,但是會(huì)比公平鎖有更高的吞吐量蚓胸。
-
公平模式
當(dāng)以公平模式初始化時(shí)挣饥,線程將會(huì)以隊(duì)列的順序獲取鎖。當(dāng)當(dāng)前線程釋放鎖后沛膳,等待時(shí)間最長(zhǎng)的寫(xiě)鎖線程就會(huì)被分配寫(xiě)鎖扔枫;或者有一組讀線程組等待時(shí)間比寫(xiě)線程長(zhǎng),那么這組讀線程組將會(huì)被分配讀鎖锹安。
源碼如下
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 鎖升級(jí)和鎖降級(jí)
- 鎖降級(jí):從寫(xiě)鎖變成讀鎖;
- 鎖升級(jí):從讀鎖變成寫(xiě)鎖短荐。
- ReentrantReadWriteLock 只支持鎖降級(jí)
- 建議盡量不要使用鎖降級(jí)操作,獲取什么鎖就回收什么鎖叹哭,同一線程盡量不要使用兩種鎖忍宋,最為安全,除非有特殊操作則需注意
2 磁盤(pán)緩存最佳設(shè)計(jì)
提供抽象類(lèi)BaseCache的源碼话速,具體實(shí)現(xiàn)大家可以通過(guò)自己的實(shí)際情況去拓展
public abstract class BaseCache {
private final ReadWriteLock mLock = new ReentrantReadWriteLock();
/**
* 讀取緩存
*
* @param key 緩存key
* @param existTime 緩存時(shí)間
*/
final <T> T load(Type type, String key, long existTime) {
//1.先檢查key
Utils.checkNotNull(key, "key == null");
//2.判斷key是否存在,key不存在去讀緩存沒(méi)意義
if (!containsKey(key)) {
return null;
}
//3.判斷是否過(guò)期讶踪,過(guò)期自動(dòng)清理
if (isExpiry(key, existTime)) {
remove(key);
return null;
}
//4.開(kāi)始真正的讀取緩存
mLock.readLock().lock();
try {
// 讀取緩存
return doLoad(type, key);
} finally {
mLock.readLock().unlock();
}
}
/**
* 保存緩存
*
* @param key 緩存key
* @param value 緩存內(nèi)容
* @return
*/
final <T> boolean save(String key, T value) {
//1.先檢查key
Utils.checkNotNull(key, "key == null");
//2.如果要保存的值為空,則刪除
if (value == null) {
return remove(key);
}
//3.寫(xiě)入緩存
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 是讓子類(lèi)不能被重寫(xiě),只能使用doContainsKey
* 這里加了鎖處理泊交,操作安全。<br>
*
* @param key 緩存key
* @return 是否有緩存
*/
public final boolean containsKey(String key) {
mLock.readLock().lock();
try {
return doContainsKey(key);
} finally {
mLock.readLock().unlock();
}
}
/**
* 是否包含 采用protected修飾符 被子類(lèi)修改
*/
protected abstract boolean doContainsKey(String key);
/**
* 是否過(guò)期
*/
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();
}