在多線程中如何保證集合的安全


線程和進程

進程(Process)的概念。狹義的進程是正在運行的程序的實例;廣義的進程是一個具有一定獨立功能的程序關於某個數據集合的一次運行活動,是操作系統動態執行的基本單元。

線程(Thread),有時被稱為輕量級進程(LWP),是程序執行流的最小單位;一個標准的線程由線程ID、當前指令指針(PC)、寄存器集合和堆棧組成。

通常情況下,一個進程由一個到多個線程組成,各個線程之間共享程序的內存空間及一些進程級的資源。

在大多數軟件應用中,線程的數量都不止一個,多線程程序處在一個多變的環境中,可訪問的全局變量和堆數據隨時都可能被其他的線程改變,這就將“線程安全”的問題提上了議程。那么,如何確保線程的安全?

 

確保線程安全的方法

一般說來,基於Linux操作系統,確保線程安全的方法有這幾個:競爭與原子操作、同步與鎖、可重入、過度優化

 

競爭與原子操作
多個線程同時訪問和修改一個數據,可能造成很嚴重的后果。出現嚴重后果的原因是很多操作被操作系統編譯為匯編代碼之后不止一條指令,因此在執行的時候可能執行了一半就被調度系統打斷了而去執行別的代碼了。一般將單指令的操作稱為原子的(Atomic),因為不管怎樣,單條指令的執行是不會被打斷的。

 

因此,為了避免出現多線程操作數據的出現異常,Linux系統提供了一些常用操作的原子指令,確保了線程的安全。但是,它們只適用於比較簡單的場合,在復雜的情況下就要選用其他的方法了。

 

同步與鎖
為了避免多個線程同時讀寫一個數據而產生不可預料的后果,開發人員要將各個線程對同一個數據的訪問同步,也就是說,在一個線程訪問數據未結束的時候,其他線程不得對同一個數據進行訪問。

 

同步的最常用的方法是使用(Lock),它是一種非強制機制,每個線程在訪問數據或資源之前首先試圖獲取鎖,並在訪問結束之后釋放鎖;在鎖已經被占用的時候試圖獲取鎖時,線程會等待,直到鎖重新可用。

 

二元信號量是最簡單的一種鎖,它只有兩種狀態:占用與非占用,它適合只能被唯一一個線程獨占訪問的資源。對於允許多個線程並發訪問的資源,要使用多元信號量(簡稱信號量)。

 

可重入
一個函數被重入,表示這個函數沒有執行完成,但由於外部因素或內部因素,又一次進入該函數執行。一個函數稱為可重入的,表明該函數被重入之后不會產生任何不良后果。可重入是並發安全的強力保障,一個可重入的函數可以在多線程環境下放心使用。

 

過度優化
在很多情況下,即使我們合理地使用了鎖,也不一定能夠保證線程安全,因此,我們可能對代碼進行過度的優化以確保線程安全。

 

我們可以使用volatile關鍵字試圖阻止過度優化,它可以做兩件事:第一,阻止編譯器為了提高速度將一個變量緩存到寄存器而不寫回;第二,阻止編譯器調整操作volatile變量的指令順序。

 

在另一種情況下,CPU的亂序執行讓多線程安全保障的努力變得很困難,通常的解決辦法是調用CPU提供的一條常被稱作barrier的指令,它會阻止CPU將該指令之前的指令交換到barrier之后,反之亦然。

 

在循環中修改集合數據的問題

看代碼:

List asList = Arrays.asList(1,2);
  for(int i:asList){
  asList.add(3);
  System.err.println(i);
}

我們執行這段代碼會拋出這個異常 java.lang.UnsupportedOperationException,這個異常告訴我們,不支持在迭代中修改當前的集合(增加或刪除元素)

為什么會這樣?那我們應該如果避免這個問題呢?

1、foreach循環中是不支持對集合中的元素刪除或添加的。

2、使用Arrays.asList產生的集合不支持在循環中更改;使用new ArrayList是可以的;但是我們不應該使用這種寫法;因為元素的添加會移除會導致集合的長度發生變化;這極容易導致BUG;並且不易排查。

3、如果確實需要在循環中移除元素; 可以考慮使用迭代器進行操作Iterator

但是如果我們在一 個多線程的程序中使用了一個共享的集合;我們 怎么能保證其它線和不更改集合中的元素?
  可以使用 Collections.synchronizedList獲取一個線程安全的集合,要想在循環集合的時候保證線程的安全必須在循環的外層為所需要循環或迭代的集合添加鎖控制;即 獲取當前集合持有的鎖進行訪問控制;
代碼:
final static List asList = Collections.synchronizedList(new ArrayList(Arrays.asList("a","b")));
public static void main(String[] args) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
          Thread.sleep(150);
          }
       catch (
          InterruptedException e) { } reload(); } }).start();
synchronized (asList) { for(String e:asList){ try {Thread.sleep(100);}     
       catch (InterruptedException e1) {}    System.out.println(e); } } } public static void reload(){ asList.clear(); asList.addAll(Arrays.asList("a","b","c")); }

注意:如果不使用Collections.synchronizedList獲取一個線程安全的集合;就必須在修改集合之前首先獲取集合的鎖進行同步控制;或者可以使用java.util.concurrent包下的相關線程安全的集合類。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM