【并發(fā)技術12】線程鎖技術的使用
線程鎖好比傳統(tǒng)線程模型中的 synchronized 技術,但是比 synchronized 方式更加面向?qū)ο螅c生活中的鎖類似,鎖本身也應該是個對象。兩個線程執(zhí)行的代碼片段如果要實現(xiàn)同步互斥的效果,它們必須用用一個鎖對象。鎖是上在代表要做操的資源的類的內(nèi)部方法中,而不是線程代碼中。這篇文章主要總結(jié)一下線程鎖技術中 Lock鎖、ReadWriteLock 鎖的使用。
1. Lock的簡單使用
有了synchronized 的基礎,Lock 就比較簡單了,首先看一個實例:
public?class?LockTest?{
public?static?void?main(String[] args)?{
new?LockTest().init();
}
private?void?init()?{
final Outputer outputer =?new?Outputer();
// 線程1打印:duoxiancheng
new?Thread(new?Runnable() {
@Override
public?void?run()?{
while?(true) {
try?{
Thread.sleep(5);
}?catch?(InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
outputer.output("duoxiancheng");
}
}
}).start();
;
// 線程2打印:eson15
new?Thread(new?Runnable() {
@Override
public?void?run()?{
while?(true) {
try?{
Thread.sleep(5);
}?catch?(InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
outputer.output("eson15");
}
}
}).start();
;
}
// 自定義一個類,保存鎖和待執(zhí)行的任務
static?class?Outputer?{
Lock?lock?=?new?ReentrantLock();?//定義一個鎖,Lock是個接口,需實例化一個具體的Lock
//字符串打印方法,一個個字符的打印
public?void?output(String name)?{
int?len = name.length();
lock.lock();
try?{
for?(int?i =?0; i < len; i++) {
System.out.print(name.charAt(i));
}
System.out.println("");
}?finally?{
lock.unlock();?//try起來的原因是萬一一個線程進去了然后掛了或者拋異常了,那么這個鎖根本沒有釋放
}
}
}
這個例子和前面介紹 synchronized 的例子差不多,區(qū)別在于將 synchronized 改成了 lock。從程序中可以看出,使用 Lock 的時候,需要先 new 一個 Lock 對象,然后在線程任務中需要同步的地方上鎖,但是一定要記得放鎖,所以使用 try 塊去處理了一下,將放鎖的動作放在 finally 塊中了。
這是一個線程任務的情況,如果兩個線程任務也不麻煩,還是在這個類中新建一個任務方法,因為 Lock 是這個類的成員變量,還是可以用這個 lock,而且必須用這個 lock,因為要實現(xiàn)同步互斥,必須使用同一把鎖。
2. 讀寫鎖的妙用
鎖又分為讀鎖和寫鎖,讀鎖與讀鎖不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥,這是由 jvm 自己控制的。這很好理解,讀嘛,大家都能讀,不會對數(shù)據(jù)造成修改,只要涉及到寫,那就可能出問題。我們寫代碼的時候只要在掙錢的位置上相應的鎖即可。讀寫鎖有個接口叫 ReadWriteLock,我們可以創(chuàng)建具體的讀寫鎖實例,通過讀寫鎖也可以拿到讀鎖和寫鎖。下面看一下讀寫鎖的例子。
public?class?ReadWriteLockTest?{
public?static?void?main(String[] args)?{
final Queue3 q3 =?new?Queue3();?//封裝共享的數(shù)據(jù)、讀寫鎖和待執(zhí)行的任務的類
for?(int?i =?0; i 3; i++) {
new?Thread() {?// 開啟三個線程寫數(shù)據(jù)
public?void?run()?{
while?(true) {
q3.put(new?Random().nextInt(10000));
}
}
}.start();
new?Thread() {?// 開啟三個線程讀數(shù)據(jù)
public?void?run()?{
while?(true) {
q3.get();
}
}
}.start();
}
}
}
class?Queue3?{
private?Object data =?null;?// 共享的數(shù)據(jù)
private?ReadWriteLock rwl =?new?ReentrantReadWriteLock();// 定義讀寫鎖
// 讀取數(shù)據(jù)的任務方法
public?void?get()?{
rwl.readLock().lock();?// 上讀鎖
try?{
System.out.println(Thread.currentThread().getName()
+?":before read: "?+ data);?// 讀之前打印數(shù)據(jù)顯示
Thread.sleep((long) (Math.random() *?1000));?// 睡一會兒~
System.out.println(Thread.currentThread().getName()
+?":after read: "?+ data);?// 讀之后打印數(shù)據(jù)顯示
}?catch?(InterruptedException e) {
e.printStackTrace();
}?finally?{
rwl.readLock().unlock();// 釋放讀鎖
}
}
// 寫數(shù)據(jù)的任務方法
public?void?put(Object data)?{
rwl.writeLock().lock();?// 上寫鎖
try?{
System.out.println(Thread.currentThread().getName()
+?":before write: "?+?this.data);?// 讀之前打印數(shù)據(jù)顯示
Thread.sleep((long) (Math.random() *?1000));?// 睡一會兒~
this.data = data;?//寫數(shù)據(jù)
System.out.println(Thread.currentThread().getName()
+?":after write: "?+?this.data);?// 讀之后打印數(shù)據(jù)顯示
}?catch?(InterruptedException e) {
e.printStackTrace();
}?finally?{
rwl.writeLock().unlock();// 釋放寫鎖
}
}
}
為了說明讀鎖和寫鎖的特點(讀鎖和讀鎖不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥),我先把上面兩個任務方法中上鎖和放鎖的四行代碼注釋掉,來看一下運行結(jié)果。
其實不管是注釋調(diào)讀鎖還是注釋調(diào)寫鎖,還是全注釋掉,都會出問題,寫的時候會有線程去讀。那么將讀寫鎖加上后,再看一下運行結(jié)果。
可以看出,有了讀寫鎖,各個線程運行有序,從結(jié)果來看,也印證了讀鎖和讀鎖不互斥,寫鎖與讀鎖、寫鎖都互斥的特點。
現(xiàn)在使用讀寫鎖寫一個模擬緩存數(shù)據(jù)的 demo,實現(xiàn)功能如下:現(xiàn)在有5個線程都需要拿數(shù)據(jù),一開始是沒有數(shù)據(jù)的,所以最先去拿數(shù)據(jù)的那個線程發(fā)現(xiàn)沒數(shù)據(jù),它就得去初始化一個數(shù)據(jù),然后其他線程拿數(shù)據(jù)的時候就可以直接拿了。代碼如下。
public?class?ReadWriteLockTest2?{
public?static?void?main(String[] args)?{
CacheData cache =?new?CacheData();
for(int?i =?1; i <=?5; i ++) {?//開啟5個線程
new?Thread(new?Runnable() {
@Override
public?void?run()?{
cache.processCache();?//都去拿數(shù)據(jù)
}
}).start();
}
}
}
class?CacheData?{
private?Object data =?null;?// 需要緩存的數(shù)據(jù)
private?boolean cacheValid;?//用來標記是否有緩存數(shù)據(jù)
private?ReadWriteLock rwl =?new?ReentrantReadWriteLock();// 定義讀寫鎖
public?void?processCache()?{
rwl.readLock().lock();?//上讀鎖
if(!cacheValid) {?//如果沒有緩存,那說明是第一次訪問,需要給data賦個值
rwl.readLock().unlock();?//先把讀鎖釋放掉
rwl.writeLock().lock();?//上寫鎖
if(!cacheValid) {
System.out.println(Thread.currentThread().getName() +?": no cache!");
data =?new?Random().nextInt(1000);?//賦值
cacheValid =?true;?//標記已經(jīng)有緩存了
System.out.println(Thread.currentThread().getName() +?": already cached!");
}
rwl.readLock().lock();?//再把讀鎖上上
rwl.writeLock().unlock();?//把剛剛上的寫鎖釋放掉
}
System.out.println(Thread.currentThread().getName() +?" get data: "?+ data);
rwl.readLock().unlock();?//釋放讀鎖
}
}
從代碼中可以看出,在 processCache 方法中對讀鎖和寫鎖的交替使用。一開始進來都是讀數(shù)據(jù)的,所以一開始都是上了讀鎖,但是當?shù)谝粋€線程進來發(fā)現(xiàn)沒有緩存數(shù)據(jù)的時候,它得寫數(shù)據(jù),那么此時它得先把讀鎖給釋放掉,換了把寫鎖,告訴其他線程:“哥們,這里面根本沒數(shù)據(jù)啊,我們被坑了,讓我先弄個數(shù)據(jù)來吧,你們等會兒~”,等該線程初始化好了數(shù)據(jù)后,其他線程就可以讀了,于是它又把讀鎖裝起來了,把寫鎖釋放了,然后它出去了。這就模擬了拿緩存數(shù)據(jù)的一個 demo,可以看出,在一個方法中,同一個線程可以操作兩個鎖的。看一下運行結(jié)果。
Thread-1: no cache!
Thread-1: already cached!
Thread-1 get data: 893
Thread-0 get data: 893
Thread-2 get data: 893
這和 Hibernate 中的那個 load(id, Class.class) 方法有點類似,先拿到的是代理對象,要使用該對象的時候,如果發(fā)現(xiàn)沒有,就新產(chǎn)生一個,如果有了就直接拿來用。
繼續(xù)進階,如果現(xiàn)在要緩存多個數(shù)據(jù),即要寫一個緩存系統(tǒng),那該如何做呢?一個緩存系統(tǒng)無非就是一個容器,可以存儲很多緩存數(shù)據(jù),很自然的想到使用一個 Map,專門裝緩存數(shù)據(jù),然后供多個線程去使用。所以整個涉及思路,跟上面緩存單個數(shù)據(jù)是一樣的,不過就是多考了一些東西而已,看下代碼。
public?class?CacheDemo?{
public?static?void?main(String[] args)?{
Cache cac =?new?Cache();
for(int?i =?0; i 3; i ++) {?//開啟三個線程去緩存中拿key為cache1的數(shù)據(jù),
new?Thread(new?Runnable() {
@Override
public?void?run()?{
String?value?= (String) cac.getData("cache1");?//第一個進入的線程要先寫一個數(shù)據(jù)進去(相當于第一次從數(shù)據(jù)庫中取)
System.out.println(Thread.currentThread().getName() +?": "?+?value);
}
}).start();
}
for(int?i =?0; i 3; i ++) {?//開啟三個線程去緩存中拿key為cacahe2的數(shù)據(jù)
new?Thread(new?Runnable() {
@Override
public?void?run()?{
String?value?= (String) cac.getData("cache2");//第一個進入的線程要先寫一個數(shù)據(jù)進去(相當于第一次從數(shù)據(jù)庫中取)
System.out.println(Thread.currentThread().getName() +?": "?+?value);
}
}).start();
}
}
}
class?Cache?{
//存儲緩存數(shù)據(jù)的Map,注意HashMap是非線程安全的,也要進行同步操作
private?Map
private?ReadWriteLock rwl =?new?ReentrantReadWriteLock();?//定義讀寫鎖
public?synchronized Object?getData(String key)?{
rwl.readLock().lock();?//上讀鎖
Object?value?=?null;
try?{
value?= cache.get(key);?//根據(jù)key從緩存中拿數(shù)據(jù)
if?(value?==?null) {?//如果第一次那該key對應的數(shù)據(jù),拿不到
rwl.readLock().unlock();?//釋放讀鎖
rwl.writeLock().lock();?//換成寫鎖
try?{
if?(value?==?null) {?//之所以再去判斷,是為了防止幾個線程同時進入了上面那個if,然后一個個都來重寫賦值一遍
System.out.println(Thread.currentThread().getName() +?" write cache for "?+ key);
value?=?"aaa"?+ System.currentTimeMillis();?// 實際中是去數(shù)據(jù)庫中取,這里只是模擬
cache.put(key,?value);?//放到緩存中
System.out.println(Thread.currentThread().getName() +?" has already written cache!");
}
}?finally?{
rwl.writeLock().unlock();?//寫完了釋放寫鎖
}
rwl.readLock().lock();?//換讀鎖
}
}?finally?{
rwl.readLock().unlock();?//最后呢釋放讀鎖
}
return?value;?//返回要取的數(shù)據(jù)
}
}
整個代碼的結(jié)構(gòu)和上面的一樣,理解了緩存單個數(shù)據(jù)后,這個代碼也不難理解。這里只是個 demo,實際中可以是跟數(shù)據(jù)庫打交道,第一次從緩存中拿肯定是沒有的,那么就要去數(shù)據(jù)庫中查,然后把取到的數(shù)據(jù)放到緩存中,下次別的線程來就能直接從緩存中取了。看一下運行結(jié)果。
Thread-0 write cache for cache1
Thread-0 has already written cache!
Thread-4 write cache for cache2
Thread-0: aaa1464782404722
Thread-4 has already written cache!
Thread-4: aaa1464782404723
Thread-3: aaa1464782404723
Thread-2: aaa1464782404722
Thread-1: aaa1464782404722
Thread-5: aaa1464782404723
從結(jié)果中可以看出,線程 0 首先去緩存中拿 key 為 cache1 的值,沒拿到,往里面寫了一個,然后線程 4 去緩存中拿 key 為 cache2 的值也沒拿到,于是也寫了一個,在此期間線程 0 把值拿了出來,后面幾個線程也隨后陸續(xù)的拿出來了。讀寫鎖的應用還是很廣泛的,而且很好用,就總結(jié)這么多吧。
任務調(diào)度 緩存
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。