除了Synchronized關鍵字還有什么可以保證線程安全?
日常使用Java開發時,多線程開發,一般就用Synchronized保證線程安全,防止並發出現的錯誤和異常,那么
除了Synchronized關鍵字還有什么可以保證線程安全嗎?
什么是線程安全?
在了解什么方法可以保證線程安全之前,我們先定義什么是線程安全。Wikipedia是如此定義的:
線程安全是程式設計中的術語,指某個函數、函數庫在多線程環境中被調用時,能夠正確地處理多個線程之間的
共享變量,使程序功能正確完成。
按我的理解,在多線程環境下程序功能正確完成,說白了就是無論單線程或者多線程環境下,這個程序運行要完全
實現了程序設計時功能的需求,沒有BUG。所以為了沒有BUG,我們必須保證單或者多線程環境下代碼運行的線程
安全。
不考慮線程安全問題行不行?
有人會說,不要多線程行不行,我只寫單線程代碼不就可以了,單線程總不會有線程安全問題了吧。假如只有單
線程,那么訪問一個網頁就會出現類似排隊買票的場景,一個請求返回了才能繼續處理下一個請求,效率非常之低。
利用多線程,可以充分利用多核CPU資源,提高性能,特別是存在計算密集型、IO密集型任務。
那么開多進程(process),一個進程對應一個線程行不行?這種情況下,基於Java語言的程序運行,每次訪問一
個請求就需要開啟一個新的JVM,再來一次class加載等等,包括JVM的優化、緩存、預熱之類的優化優勢全部沒了。
那么只開一個線程,一個線程對應多個協程(coroutine)或者微線程(fiber)行不行? 首先Java語言本身沒有提供原
生的協程實現,其次即便實現了了,只用單線程還是無法充分利用多核CPU資源,性能也會達到瓶頸。
說到底為了性能,使用多線程有益,代價就是對於一個共享並且狀態可變的資源而言,只要存在競爭條件(race
condition),線程安全問題就必須納入開發代碼時的考慮范圍。
什么情況下才不線程安全?
為了避免線程不安全,我們必須先了解什么情況下才會出現線程不安全的問題。一個變量只在線程內使用,這種
情況下需要考慮線程安全問題嗎?不需要,只有對需要在多個線程之間共享的變量的訪問操作,我們才需要考慮線程
安全問題。所有線程共享的變量都需要考慮嗎?不是,如果一個變量初始化后就不再變化,即變量為不可變狀態,我
們也無需考慮。所以,只有線程之間存在對共享可變的對象訪問操作時,才會出現線程安全問題。
如何保證線程安全?
不共享
正如前文所說,只有需要操作共享的可變對象時會出現線程安全問題,那么只要不共享對象不就就可以保證線程
安全了嗎。方法在於要確保對象本身或者其引用不會被共享出去,例如局部變量肯定是不會暴露出去,也可以利用
ThreadLocal保存並傳遞對象。
不可變(immutable)
如果必須共享,那么對象為不可變狀態,也能保證線程安全。什么情況下對象才是不可變的呢?對象構造完后所
有屬性都不會再發生改變就是不可變。以A類為例,
public class A {
private int fieldA;
private int[] fieldB;
public A(C c) {
c.setD(new D());
// 初始化
}
public void setFieldA(int a) {
fieldA = a;
}
public int[] getFieldB() {
return fieldB;
}
private void updateA() {
//修改內部狀態
}
class D {
public void updateD() {
updateA();
}
}
}
首先需要將屬性變為final,保證構造函數初始化后其他屬性不會發生變動,防止屬性狀態會被更改。
public class A {
private final int fieldA;
private final int[] fieldB;
public A(C c) {
c.setD(new D());
// 初始化
}
public int[] getFieldB() {
return fieldB;
}
private void updateA() {
//修改內部狀態
}
class D {
public void updateD() {
updateA();
}
}
}
其次防止內部的可變對象引用暴露出去。
public class A {
// 置為final狀態
private final int fieldA;
private final int[] fieldB;
public A(C c) {
c.setD(new D());
// 初始化
}
public int[] getFieldB() {
if (fieldB == null) {
return null;
}
// 防止引用暴露,外部可以修改fieldB狀態
return Arrays.copyOf(fieldB, fieldB.length);
}
private void updateA() {
//修改內部狀態
}
class D {
public void updateD() {
updateA();
}
}
}
最后構造函數時要防止A類的this逃逸(this escape),防止構造函數還沒有完成初始化時,其他線程可以利用逃逸的
this調用內部方法。
public class A {
// 置為final狀態
private final int fieldA;
private final int[] fieldB;
public A(C c) {
// updateD方法可能在c被其他線程調用,間接調用updateA方法,修改了Class A實例的狀態
c.setD(new D());
// 初始化
}
public int[] getFieldB() {
if (fieldB == null) {
return null;
}
// 防止引用暴露,外部可以修改fieldB狀態
return Arrays.copyOf(fieldB, fieldB.length);
}
private void updateA() {
//修改內部狀態
}
class D {
// 去除updateD方法
}
}
單線程修改
對象必須共享且是可變的,如果只會被其中一個線程修改,其他線程只是讀取,那么只需要對該對象其屬性狀態添加volatile
關鍵字修飾,保證內存可見性,其他線程能讀取到對象的最新狀態,這樣也可以保證線程安全。
將對象屬性修改為線程安全類型
public class A {
private Map<Integer, B> fieldB;
public A() {
fieldB = new HashMap<>();
}
public B getB(Integer key) {
return fieldB.get(key);
}
public B addB(Integer key, B value) {
B b = fieldB.get(key);
if (b == null) {
fieldB.put(key, value);
}
return b;
}
}
將線程安全委托給屬性對象,由屬性對象保證線程安全
public class A {
private final Map<Integer, B> fieldB;
public A() {
fieldB = new ConcurrentHashMap<>();
}
public B getB(Integer key) {
return fieldB.get(key);
}
public B addB(Integer key, B value) {
return fieldB.putIfAbsent(key, value);
}
}
使用synchronized關鍵字
對於共享的可變對象,要求所有屬性狀態修改讀取都用synchronized修飾
public class A {
private B fieldB;
public synchronized B getB() {
return fieldB;
}
public synchronized void updateB() {
// 其他操作
fieldB.update();
// 其他操作
}
}
可以優化使用內部的私有對象作為鎖對象,避免鎖住整個方法
public class A {
private B fieldB;
private final Object lock = new Object();
public synchronized B getB() {
return fieldB;
}
public void updateB() {
// 其他操作
synchronized (lock) {
fieldB.update();
}
// 其他操作
}
}