本篇是《Java虛擬機併發編程》第五章的閱讀筆記
在(三)中,我們在代碼裏沒有使用任何顯示的同步操作,直接作用在可變變量上,當然是因爲在程序中只有一個可變字段。如果程序中不止一個與可變狀態相關或依賴的變量,那麼我們就無可避免地要使用顯示的同步操作。
到目前爲止重構都達到了預想的效果,但我們還要想更高要求的目標邁進:
- 追蹤並記錄電源的使用情況。即每次電源電量消耗完畢的時候,我們度需要把電源的使用次數進行累加
當然這也意味着,我們必須保證對於變量level和useage的改動是原子的。(在同一個線程裏修改這個兩個變量,要麼全部修改成功,要麼全部都不變)
我們這裏選擇使用ReentrantReadWriteLock。該類同時提供兩把鎖(即讀鎖和寫鎖)。由於使用了顯示的鎖,所以就可以將level字段由AtomicLong改回long類型。
package com.ensureatomicity;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class EnergySource {
private final long MAXLEVEL = 100;
private long level = MAXLEVEL;
private long usage = 0;
private final ReadWriteLock monitor = new ReentrantReadWriteLock();
private static final ScheduledThreadPoolExecutor replenishTimer =
new ScheduledThreadPoolExecutor(10);
private ScheduledFuture<?> replenishTask;
private EnergySource(){}
private void init(){
replenishTask = replenishTimer.scheduleAtFixedRate(new Runnable(){
public void run(){
System.out.println(System.nanoTime()/1.0e9);
replenish();
}
}, 0, 1, TimeUnit.SECONDS);
}
public static EnergySource create(){
final EnergySource energySource = new EnergySource();
energySource.init();
return energySource;
}
public long getUnitsAvailable(){
monitor.readLock().lock();
try{
return level;
}finally{
monitor.readLock().unlock();
}
}
public long getUsageCount(){
monitor.readLock().lock();
try{
return usage;
}finally{
monitor.readLock().unlock();
}
}
public boolean useEnergy(final long units){
monitor.writeLock().lock();
try{
if(units>0 && level>=units){
level -= units;
usage++;
return true;
}else{
return false;
}
}finally{
monitor.writeLock().lock();
}
}
public void stopEnergySource(){
replenishTask.cancel(false);
}
private void replenish(){
monitor.writeLock().lock();
try{
if(level < MAXLEVEL){
level++;
}
}finally{
monitor.writeLock().unlock();
}
}
}
- 在上面的代碼中,引入兩個新字段:usage和monitor。其中usage的作用是記錄電源被使用的次數。
在useEnergy函數中,我們先獲得了寫鎖,如果可以的話還可以在獲得鎖的時候指定一個超時時間。一旦獲得寫鎖的操作完成,就可以更改兩個變量的值。最後在finally塊中,安全地將鎖釋放。
遺憾的是,這一版本的代碼比之前的版本複雜了不少,而這錯誤正是由複雜性所產生的。當然還有方法可以避免使用顯示的同步操作。
ReentrantReadWriteLock簡單介紹
- 讀鎖,可以使得讀操作併發執行,但是如果巧合有線程在進行寫操作,那麼該讀操作會被阻塞。
寫線程獲取寫入鎖後可以再次獲取讀取鎖,但是讀線程獲取讀取鎖後卻不能獲取寫入鎖,要獲取寫入鎖,只能先釋放讀取鎖。
可以參考別人寫的ReentrantReadWriteLock 可重入讀寫鎖的理解和ReentrantReadWriteLock可重入讀寫鎖分析