Java(8-4)多線程各種細節問題1

上一次我們介紹了Volatile,監視器還有synchronized鎖關鍵字的使用。這一次我們要說下關於final在多線程的作用,原子性的使用,死鎖以及Java中的應對方案,線程的局部變量 和 讀寫鎖的介紹 。

Part 1 關於final變量在多線程的使用

我們如今已經瞭解到,除非使用鎖或volatile修飾符,否則無法從多個線程安全地讀取一個域。
但是還有一種情況可以安全的訪問一個共享域,即這個域聲明爲final時。

final Map<String,Double> accounts = new HashMap();

這樣子,其他線程會在構造函數完成構造之後纔看到這個accounts變量。
如果不使用final,就不能保證其他線程看到的是accounts更新後的值,它們可能都只是看到null,而不是新構造的HashMap。
當然,對這個映射表的訪問是安全的,但是並不意味對他的操作是安全的!如果多個線程在讀寫這個映射表,仍然要進行同步。

Part 2 原子性
假設對共享變量除了賦值之外並不完成其他操作,那麼可以將這些共享變量聲明爲volatile(他就是一個簡單賦值,而不是讀取 增加某個數 再賦值 ,所以他是原子操作! 是 x = 1 而不是 x = x +1)。

但是我們還有更好的原子操作的表示方法,請看:
在java.util.concurrent.atomic包中有很多類使用了高效的機器指令(而不是鎖)來保證其他操作的原子性。
例如:AtommicInteger類提供了方法incrementAndGet 和 decrementAndGet,它們分別以原子方式將一個整數自增或自減。例如,可以安全地生成一個數值序列,如下所示:

public static AtomicLong nextNumber = new AtomicLong();
// In some thread...
long id = nextNumber.incrementAndGet();

incrementAndGet方法以原子方式將將AtomicLong自增,並返回自增後的值。可以保證,即使多個線程訪問一個實例,也會計算並返回正確的值。
有很多方法可以以原子方式設置和增減值,不過,如果希望完成更復雜的更新,就必須使用compareAndSet方法。例如,假設希望跟蹤不同線程觀察最大值。下面的代碼應該是這樣的:

do{
    oldValue = largest.get();
    newValue = Math.max(oldValue,observed);
}while(!largest.compareAndSet(oldValue,newValue))

這樣子的話,如果另一個線程也在更新largest,就可能阻止這個線程更新。這樣一來,compareAndSet就會返回false,而不是設置新值。這種情況下,循環會再次嘗試,讀取更新後的值,並嘗試修改。最終,他會成功地用新值替換原來的值。
accumulateAndGet方法利用一個二元操作符來合併原子值和所提供的參數。
還有getAndUpate和getAndAccumulate。

如果有大量的線程要訪問相同的原子值,性能會大大下降,因爲樂觀更新需要太多次重試。Java 8 針對這點提供了LongAdder 和 LongAccumulator類來解決這個問題。LongAdder包括多個變量(加數),其綜合爲當前值。可以有多個線程更新不同的加數,線程個數增加時會自動提供新的加數。代碼如下:

final LongAdder adder = new LongAdder();
for(...)
    pool.submit(()->{
        while(...){
            ...
            if(...)adder.increment();
        }
    });
    ...
    long total = adder.sum());

LongAccumulator將這種思想推廣到任意的累加操作中。在構造器中,可以提供這個操作以及它的零元素。要加入新的值,可以調用accumulate。調用get來獲得當前值。

Part 3 死鎖以及Java關於死鎖的應對(鎖測試和超時):

鎖和條件不能解決多線程中的所有問題比如,死鎖(考慮下哲學家問題)。
遺憾的是java中並沒有能完全避免死鎖的方法,但是我們可以通過自己的設計和良好的習慣來避免死鎖。

這裏就可能需要用到測試鎖了:線程在調用lock方法獲得另一個線程持有的鎖的時候,很可能發生阻塞,甚至發生死鎖。trylock方法試圖去申請一個鎖,在成功獲得鎖後會返回一個true,否則,立即返回false,而且線程可以立即離開去做其他事情。

if(mylock.trylock()){
    // 現在已經上鎖
    try{...}
    finally{ mylock.unlock();}
}
else
// do something else

可以調用tryLock時,使用超時參數,像這樣:if(mylock.tryLock(100,TimeUnit.MILLISECONDS))

TimUnit是個枚舉類,可取的值包括SECONDS,MILLISECONDS,MICROSECONDS和NANOSECONDS。

有趣的地方在這裏,如果一個線程調用帶有超時參數的tryLock,同時調用後如果線程在等待期間被中斷,將拋出一個InterruptedException異常。這是一個非常有用的特性,因爲允許程序打破死鎖!

而調用lockInterruptibly方法,就相當於一個超時設爲無限的tryLock方法。
在等待條件對象時候也可以提供一個超時:

myCondition.await(100,TimeUnit.MILLISECONDS)

Part 4 線程局部變量:
前面幾節中,我們討論了在線程間共享變量的風險。有時可能要避免共享變量,使用ThreadLocal輔助類爲各個線程提供各自的實例。例如,SimpleDateFormat類不是線程安全的。假設有一個靜態變量:

public static final SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd");

如果兩個線程都執行以下操作:

String dateStamp = dateFormat.format(new Date());

結果很可能會混亂,因爲dateFormat使用內部數據結構可能會被併發的訪問破壞。當然可以使用同步,但開銷很大;或者也可以在需要時構造一個局部SimoleDateFormat對象,不過這也太浪費了。

這時候,我們就可以爲每一個線程構造一個實例,如下:

public static final ThreadLocal<SimpleDateFormat> dateFormat = 
ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd"));

要是說具體的格式化方法,可以調用:

String dateStamp = dateFormat.get().format(new Date());

在一個給定線程中首次調用get時,會調用initialValue方法。在此之後,get方法會返回屬於當前線程的那個實例。

Part 讀寫鎖:
java.until.concurrent.locks包定義了兩個鎖類,我們已經討論的ReentrantLock類和ReentranReandWriteLock類。如果很多線程從一個數據結構讀取數據而很少線程修改其中數據的話,後者是十分有用的。在這種情況下,允許讀者線程共享訪問是合適的,當然,寫者線程依然必須是互斥訪問。
下面是讀寫鎖的必要步驟:
(1)構造一個ReentranReandWriteLock對象;

private ReetrantReadWriteLock rwl = new ReentrantReanWriteLock();

(2)抽取讀鎖和寫鎖;

private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();

(3)多所有的獲取方法加讀鎖;

public double getTotalBalance()
{
    readLock.lock();
    try{...}
    finally{readLock.unlock();}
}

(4)對所有的修改方法加寫鎖;

public void transfer(...)
{
    writeLock.lock();
    try{...}
    finally{writeLock.unLock();}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章