同步容器(如Vector)並不是所有操作都執行緒安全!~

HollisChuang發表於2018-08-23

之前在公眾號(Hollis)中問了這個問題:對於執行緒安全的集合類(例如Vector)的任何操作是不是都能保證執行緒安全?

三天之內收到120+回覆,其中表示不清楚的大概有10人左右,認為可以保證執行緒安全的有大概70人左右,認為不能保證執行緒安全的有50人左右,這其中能給出明確解釋的有5人。 分別是:

@趙鵬:

size方法和get方法,如果集合的長度變化了,可能丟擲異常,

@aold619:

去網上查了資料:“有條件的執行緒安全 我們在 7 月份的檔案“ 併發集合類”中討論了有條件的執行緒安全。有條件的執行緒安全類對於單獨的操作可以是執行緒安全的,但是某些操作序列可能需要外部同步。條件執行緒安全的最常見的例子是遍歷由 Hashtable 或者 Vector 或者返回的迭代器 -- 由這些類返回的 fail-fast 迭代器假定在迭代器進行遍歷的時候底層集合不會有變化。為了保證其他執行緒不會在遍歷的時候改變集合,進行迭代的執行緒應該確保它是獨佔性地訪問集合以實現遍歷的完整性。通常,獨佔性的訪問是由對鎖的同步保證的 -- 並且類的文件應該說明是哪個鎖(通常是物件的內部監視器(intrinsic monitor))。 如果對一個有條件執行緒安全類進行記錄,那麼您應該不僅要記錄它是有條件執行緒安全的,而且還要記錄必須防止哪些操作序列的併發訪問。使用者可以合理地假設其他操作序列不需要任何額外的同步。”

@閆曉琦:

答,不是,經常會出現陣列越界報錯

@逆風飛揚:

vector單個的方法 synchronized並不代表vector組合的方法呼叫具有原子性。有組合的操作還是需要針對vector進行加鎖。

@慕容:

不是,執行緒安全集合只能保證單個操作安全,複合操作同樣不安全

那麼這個問題的正解應該是什麼的。

問:對於執行緒安全的集合類(例如Vector)的任何操作是不是都能保證執行緒安全?

答:同步容器中的所有自帶方法都是執行緒安全的,因為方法都使用synchronized關鍵字標註。但是,對這些集合類的複合操作無法保證其執行緒安全性。需要客戶端通過主動加鎖來保證

如果你看過JDK的原始碼,那麼你會發現,像Vector這樣的同步容器的所有共有方法全都是synchronized的。也就是說,我們可以在多執行緒場景中放心的使用單獨這些方法,因為這些方法本身的確是執行緒安全的。那麼為什麼又說複合操作無法保證執行緒安全呢?這裡舉個栗子,我們定義如下刪除Vector中最後一個元素方法:

public Object deleteLast(Vector v){
    int lastIndex  = v.size()-1;
    v.remove(lastIndex);
}
複製程式碼

上面這個方法是一個複合方法,包括size()remove(),乍一看上去好像並沒有什麼問題,無論是size()方法還是remove()方法都是執行緒安全的,那麼整個deleteLast方法應該也是執行緒安全的。但是時,如果多執行緒呼叫該方法的過程中有,remove方法有可能丟擲ArrayIndexOutOfBoundsException。我們看一下remove方法具體實現,什麼情況下會丟擲這個異常呢。

public synchronized E remove(int index) {
    modCount++;
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    E oldValue = elementData(index);

    int numMoved = elementCount - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--elementCount] = null; // Let gc do its work

    return oldValue;
}
複製程式碼

從上面程式碼中可以看出,當index >= elementCount時,會丟擲ArrayIndexOutOfBoundsException,也就是說,噹噹前索引值不再有效的時候,將會丟擲這個異常。因為removeLast方法,有可能被多個執行緒同時執行,當執行緒一通過index()獲得索引值為10,在嘗試通過remove()刪除該索引位置的元素之前,執行緒2把該索引位置的值刪除掉了,這時執行緒一在執行時便會丟擲異常。

為了避免出現類似問題,可以嘗試加鎖:

public void deleteLast() {
    synchronized (v) {
        int index = v.size() - 1;
        v.remove(index);
    }
}
複製程式碼

如上,我們在deleteLast中,對v進行加鎖,即可保證同一時刻,不會有其他執行緒刪除掉v中的元素。

至此,我們已經解釋清楚了我們的問題。

問:對於執行緒安全的集合類(例如Vector)的任何操作是不是都能保證執行緒安全?

答:同步容器中的所有自帶方法都是執行緒安全的,因為方法都使用synchronized關鍵字標註。但是,對這些集合類的複合操作無法保證其執行緒安全性。需要客戶端通過主動加鎖來保證。

由於我們自己已知Vector等同步容器是執行緒安全的,所以我們通常在多執行緒場景中會直接拿來使用,並不會考慮太多,從而可能導致問題。

所以,我們在使用同步容器的時候,如果只使用其中的自帶方法,那麼可以放心使用,因為他們是執行緒安全的,但是如果我們想做複合操作,尤其是涉及到刪除容器中的元素時,一定要注意是否需要客戶端主動加鎖。

下面,我們考慮以下程式碼,如果在多執行緒場景中使用會不會出現執行緒安全問題:

for (int i = 0; i < v.size(); i++) {
    System.out.println(v.get(i));
}
複製程式碼

顯然,以上程式碼在迭代的過程中,並不會出現執行緒安全問題。但是,如果在程式中還有以下程式碼有可能被同時呼叫呢?

for (int i = 0; i < v.size(); i++) {
    v.remove(i);
}
複製程式碼

由於,不同執行緒在同一時間操作同一個Vector,其中包括刪除操作,那麼就同樣有可能發生執行緒安全問題。所以,在使用同步容器的時候,如果涉及到多個執行緒同時執行刪除操作,就要考慮下是否需要加鎖。

wechat

相關文章