因此,我將嘗試定義不變性及其與線程安全性的關系。
定義 不變性
我的定義是“不可變的對象是在構造之后狀態不會改變的對象”。 我故意含糊其詞,因為沒有人真正同意確切的定義。
線程安全
您可以在Internet上找到許多不同的“線程安全”定義。 定義它實際上非常棘手。 我會說線程安全代碼是在多線程環境中具有預期行為的代碼。 我讓您定義“預期行為”…
字符串示例
讓我們看一下String
的代碼(實際上只是一部分代碼……):
public class String {private final char value[];/** Cache the hash code for the string */private int hash; // Default to 0public String(char[] value) {this.value = Arrays.copyOf(value, value.length);}public int hashCode() {int h = hash;if (h == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {h = 31 * h + val[i];}hash = h;}return h;}
}
String
被認為是不可變的。 看一下它的實現,我們可以推斷出一件事:不可變的對象可以更改其內部狀態(在這種情況下,是延遲加載的哈希碼),只要它在外部不可見即可。
現在,我將以一種非線程安全的方式重寫hashcode方法:
public int hashCode() {if (hash == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {hash = 31 * hash + val[i];}}return hash;}
如您所見,我刪除了局部變量h
并直接影響了變量hash
。 此實現不是線程安全的! 如果多個線程同時調用hashcode
,則每個線程的返回值可能不同。 問題是,這堂課是一成不變的嗎? 由于兩個不同的線程可以看到不同的哈希碼,因此從外部角度來看,我們具有狀態更改,因此它不是不可變的。
我們可以得出這樣的結論: String
是不可變的, 因為它是線程安全的,而不是相反的。 所以……說“做一些不可變的對象,它是線程安全的!”有什么意義? 但是請注意,您必須使不可變對象具有線程安全性!” ?
ImmutableSimpleDateFormat示例
在下面,我寫了一個類似于SimpleDateFormat的類。
public class VerySimpleDateFormat {private final DateFormat formatter = SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT);public String format(Date d){return formatter.format(d);}
}
該代碼不是線程安全的,因為SimpleDateFormat.format
不是。
這個對象是不變的嗎? 好問題! 我們已盡力使所有字段均不可修改,我們不使用任何設置方法或任何建議對象狀態將改變的方法。 實際上,內部SimpleDateFormat
更改其狀態,這就是它不安全線程的原因。 由于對象圖中的某些內容發生了變化,因此即使它看起來像它也不是不變的。問題甚至不是SimpleDateFormat
更改其內部狀態,而是它以一種非線程安全的方式進行操作。
總結這個例子,創建一個不可變的類并不容易。 最后一個關鍵字還不夠,您必須確保對象的對象字段不會更改其狀態,這有時是不可能的。
不可變的對象可以具有非線程安全的方法(沒有魔術!)
讓我們看一下下面的代碼。
public class HelloAppender {private final String greeting;public HelloAppender(String name) {this.greeting = 'hello ' + name + '!\n';}public void appendTo(Appendable app) throws IOException {app.append(greeting);}
}
HelloAppender
類絕對是不可變的。 方法appendTo接受Appendable
。 由于Appendable
不能保證是線程安全的(例如StringBuilder
),因此追加到此Appendable
會在多線程環境中引起問題。
結論
在某些情況下,創建不可變對象絕對是一個好習慣,并且對創建線程安全代碼有很大幫助。 但是,當我到處閱讀時,這使我感到困擾。 不可變對象是線程安全的 ,顯示為公理。 我明白了這一點,但是我認為對此進行一點思考總是很有益的,以便理解導致非線程安全代碼的原因。
感謝Jose的評論,在本文的結尾我得出了不同的結論。 這都是關于不可變的定義。 需要澄清!
如果滿足以下條件,則對象是不可變的:
- 它的所有字段在使用之前都已初始化(這意味著您可以進行延遲初始化)
- 字段的狀態在初始化后不會更改(不更改表示對象圖不會更改,即使子級的內部狀態也是如此)
除非對象必須處理非線程安全的對象,否則不可變對象將始終是線程安全的。
參考: 不變性真的意味著線程安全嗎? 從我們的JCG合作伙伴 Tibo Delor在InvalidCodeException博客中獲得。
翻譯自: https://www.javacodegeeks.com/2012/09/does-immutability-really-means-thread.html