安卓開發(Java)中關於final關鍵字與執行緒安全性

weixin_33816946發表於2018-09-15

前言

學習新知識固然重要,但是時常往回看看,溫故知新是很必要的。回顧一下執行緒安全性和final關鍵字。

正文

從Java 5開始,final keyword一個特殊用法是在併發庫中一個非常重要且經常被忽視的武器。實質上,可以使用final來確保在構造物件時,訪問該物件的另一個執行緒不會看到處於部分構造狀態的物件,否則可能會發生這種情況。這是因為當作為物件變數的一個屬性時,final作為其定義的一部分具有以下重要特徵:

當建構函式退出時,final keyword的值保證對訪問構造物件的其他執行緒可見。

使用final是所謂的安全釋出(safe publication)的一種方式,這裡,釋出(publication)一個地相意味著在一個執行緒中建立它,同時另一個執行緒在之後的某時刻可以引用到該新建立的物件。當JVM呼叫物件的建構函式時,它必須將各成員賦值,同時儲存一個指向該物件的指標。就像其他任何的資料寫入一樣,這可能是亂序的,and their application to main memory can be delayed and other processors can be delayed unless you take special steps to combat this(看不太懂,是不是說“把他們寫回主存可能推遲,並且其他的處理器(看到變化)也會推遲,要客服這一點,除非採取非常步驟”)。特別的,指向物件的引用可能在成員變數提交之前(導致如此的原因之一是編譯器的指令重排ordering:if you think about how you'd write things in a low-level language such as C or assembler, it's quite natural to store a pointer to a block of memory, and then advance the pointer as you're writing data to that block)就被寫入到主存並被訪問到了。這樣會導致另一個執行緒看到了一個不合法或不完整的物件。

而final可以防止此類事情的發生:如果某個成員是final的,JVM規範做出如下明確的保證:一旦物件引用對其他執行緒可見,則其final成員也必須正確的賦值了。

final的物件引用

物件的final成員成員的值在當退出建構函式時,他們也是最新的。這意味著:

final型別的成員變數的值,包括那些用final引用指向的collections的物件,是讀執行緒安全而無需使用synchronization的

注意,如果你有一個指向collection,陣列或其他可變物件的final引用,如果存在其他執行緒訪問,仍然需要使用同步機制來訪問該物件(或使用ConcurrentHashMap)。

因此,不可變物件(指所有的成員都是final並且成員要麼是基本型別,要麼指向另一個不可變物件)可以併發訪問而無需使用同步機制。通過final引用讀取“實際不可變”物件(指成員雖然實際並不是final,然而卻從不會改變)也是安全的。然而,從程式設計的角度來看,在此種情況下強化不可變性是明智的(如用Collections.unmodifiableList()封裝一個collection)。That way, you'll spot bugs introduced when one of your colleagues naughtily attempts to modify a collection that you didn't intend to be modified!

使用final的限制條件和侷限性

當宣告一個final成員時,必須在建構函式退出前設定它的值,如下:

public class MyClass {

    private final int myField = 3;

    public MyClass() { ... }

}

或者

public class MyClass {

    private final int myField;

    public MyClass() {

        ...

        myField = 3;

        ...

    }

}

需要強調的是將指向物件的成員宣告為final只能將該引用設為不可變的,而非所指的物件。例如如果一個list宣告如下:

private final List myList =new ArrayList();

仍然可以修改該list

myList.add("Hello");

然而,宣告為final可以保證如下操作不合法:

myList =new ArrayList();

myList= someOtherList;

什麼時候應該使用final

一個答案就是“儘可能的使用”。任何你不希望改變的(基本型別,或者指向一個物件,不管該物件是否可變)一般來講都應該宣告為final。另一種看待此問題的方式是:

如果一個物件將會在多個執行緒中訪問並且你並沒有將其成員宣告為final,則必須提供其他方式保證執行緒安全

“其他方式”可以包括宣告成員為volatile,使用synchronized或者顯式Lock控制所有該成員的訪問。

大家往往忽視的典型case是在一個執行緒建立一個物件,而後在另一個執行緒使用,如一個通過ThreadPoolExecutor的物件。這種情況下,必須保證該物件的執行緒安全性:這和執行緒的併發訪問關係不大,主要是因為在其生命週期內,不同的執行緒會在任意時刻訪問它(還是記憶體模型的問題吧)



作者:AirrWang
連結:https://www.jianshu.com/p/ba764ca54262
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。

相關文章