Part 1 關(guān)于final變量在多線程的使用
我們?nèi)缃褚呀?jīng)了解到,除非使用鎖或volatile修飾符,否則無法從多個線程安全地讀取一個域。
但是還有一種情況可以安全的訪問一個共享域,即這個域聲明為final時。
final Map《String,Double》 accounts = new HashMap();
這樣子,其他線程會在構(gòu)造函數(shù)完成構(gòu)造之后才看到這個accounts變量。
如果不使用final,就不能保證其他線程看到的是accounts更新后的值,它們可能都只是看到null,而不是新構(gòu)造的HashMap。
當(dāng)然,對這個映射表的訪問是安全的,但是并不意味對他的操作是安全的!如果多個線程在讀寫這個映射表,仍然要進(jìn)行同步。
Part 2 原子性
假設(shè)對共享變量除了賦值之外并不完成其他操作,那么可以將這些共享變量聲明為volatile(他就是一個簡單賦值,而不是讀取 增加某個數(shù) 再賦值 ,所以他是原子操作! 是 x = 1 而不是 x = x +1)。
但是我們還有更好的原子操作的表示方法,請看:
在java.util.concurrent.atomic包中有很多類使用了高效的機(jī)器指令(而不是鎖)來保證其他操作的原子性。
例如:AtommicInteger類提供了方法incrementAndGet 和 decrementAndGet,它們分別以原子方式將一個整數(shù)自增或自減。例如,可以安全地生成一個數(shù)值序列,如下所示:
public static AtomicLong nextNumber = new AtomicLong();
// In some thread.。.
long id = nextNumber.incrementAndGet();
incrementAndGet方法以原子方式將將AtomicLong自增,并返回自增后的值??梢员WC,即使多個線程訪問一個實例,也會計算并返回正確的值。
有很多方法可以以原子方式設(shè)置和增減值,不過,如果希望完成更復(fù)雜的更新,就必須使用compareAndSet方法。例如,假設(shè)希望跟蹤不同線程觀察最大值。下面的代碼應(yīng)該是這樣的:
do{
oldValue = largest.get();
newValue = Math.max(oldValue,observed);
}while(!largest.compareAndSet(oldValue,newValue))
這樣子的話,如果另一個線程也在更新largest,就可能阻止這個線程更新。這樣一來,compareAndSet就會返回false,而不是設(shè)置新值。這種情況下,循環(huán)會再次嘗試,讀取更新后的值,并嘗試修改。最終,他會成功地用新值替換原來的值。
accumulateAndGet方法利用一個二元操作符來合并原子值和所提供的參數(shù)。
還有g(shù)etAndUpate和getAndAccumulate。
如果有大量的線程要訪問相同的原子值,性能會大大下降,因為樂觀更新需要太多次重試。Java 8 針對這點提供了LongAdder 和 LongAccumulator類來解決這個問題。LongAdder包括多個變量(加數(shù)),其綜合為當(dāng)前值??梢杂卸鄠€線程更新不同的加數(shù),線程個數(shù)增加時會自動提供新的加數(shù)。代碼如下:
final LongAdder adder = new LongAdder();
for(...)
pool.submit(()-》{
while(...){
...
if(...)adder.increment();
}
});
...
long total = adder.sum());
LongAccumulator將這種思想推廣到任意的累加操作中。在構(gòu)造器中,可以提供這個操作以及它的零元素。要加入新的值,可以調(diào)用accumulate。調(diào)用get來獲得當(dāng)前值。
Part 3 死鎖以及Java關(guān)于死鎖的應(yīng)對(鎖測試和超時):
鎖和條件不能解決多線程中的所有問題比如,死鎖(考慮下哲學(xué)家問題)。
遺憾的是java中并沒有能完全避免死鎖的方法,但是我們可以通過自己的設(shè)計和良好的習(xí)慣來避免死鎖。
這里就可能需要用到測試鎖了:線程在調(diào)用lock方法獲得另一個線程持有的鎖的時候,很可能發(fā)生阻塞,甚至發(fā)生死鎖。trylock方法試圖去申請一個鎖,在成功獲得鎖后會返回一個true,否則,立即返回false,而且線程可以立即離開去做其他事情。
if(mylock.trylock()){
// 現(xiàn)在已經(jīng)上鎖
try{...}
finally{ mylock.unlock();}
}
else
// do something else
可以調(diào)用tryLock時,使用超時參數(shù),像這樣:if(mylock.tryLock(100,TimeUnit.MILLISECONDS))
TimUnit是個枚舉類,可取的值包括SECONDS,MILLISECONDS,MICROSECONDS和NANOSECONDS。
有趣的地方在這里,如果一個線程調(diào)用帶有超時參數(shù)的tryLock,同時調(diào)用后如果線程在等待期間被中斷,將拋出一個InterruptedException異常。這是一個非常有用的特性,因為允許程序打破死鎖!
而調(diào)用lockInterruptibly方法,就相當(dāng)于一個超時設(shè)為無限的tryLock方法。
在等待條件對象時候也可以提供一個超時:
myCondition.await(100,TimeUnit.MILLISECONDS)
Part 4 線程局部變量:
前面幾節(jié)中,我們討論了在線程間共享變量的風(fēng)險。有時可能要避免共享變量,使用ThreadLocal輔助類為各個線程提供各自的實例。例如,SimpleDateFormat類不是線程安全的。假設(shè)有一個靜態(tài)變量:
public static final SimpleDateFormat dataFormat = new SimpleDateFormat(“yyyy-MM-dd”);
如果兩個線程都執(zhí)行以下操作:
String dateStamp = dateFormat.format(new Date());
結(jié)果很可能會混亂,因為dateFormat使用內(nèi)部數(shù)據(jù)結(jié)構(gòu)可能會被并發(fā)的訪問破壞。當(dāng)然可以使用同步,但開銷很大;或者也可以在需要時構(gòu)造一個局部SimoleDateFormat對象,不過這也太浪費了。
這時候,我們就可以為每一個線程構(gòu)造一個實例,如下:
public static final ThreadLocal《SimpleDateFormat》 dateFormat =
ThreadLocal.withInitial(()-》new SimpleDateFormat(“yyyy-MM-dd”));
要是說具體的格式化方法,可以調(diào)用:
String dateStamp = dateFormat.get().format(new Date());
在一個給定線程中首次調(diào)用get時,會調(diào)用initialValue方法。在此之后,get方法會返回屬于當(dāng)前線程的那個實例。
Part 讀寫鎖:
java.until.concurrent.locks包定義了兩個鎖類,我們已經(jīng)討論的ReentrantLock類和ReentranReandWriteLock類。如果很多線程從一個數(shù)據(jù)結(jié)構(gòu)讀取數(shù)據(jù)而很少線程修改其中數(shù)據(jù)的話,后者是十分有用的。在這種情況下,允許讀者線程共享訪問是合適的,當(dāng)然,寫者線程依然必須是互斥訪問。
下面是讀寫鎖的必要步驟:
?。?)構(gòu)造一個ReentranReandWriteLock對象;
private ReetrantReadWriteLock rwl = new ReentrantReanWriteLock();
?。?)抽取讀鎖和寫鎖;
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
?。?)多所有的獲取方法加讀鎖;
public double getTotalBalance()
{
readLock.lock();
try{...}
finally{readLock.unlock();}
}
(4)對所有的修改方法加寫鎖;
public void transfer(...)
{
writeLock.lock();
try{...}
finally{writeLock.unLock();}
}
評論