1. 程式人生 > >實現多執行緒安全的3種方式

實現多執行緒安全的3種方式

實現多執行緒安全的3種方式

 

1、先來了解一下:為什麼多執行緒併發是不安全的?

  在作業系統中,執行緒是不擁有資源的,程序是擁有資源的。而執行緒是由程序建立的,一個程序可以建立多個執行緒,這些執行緒共享著程序中的資源。所以,當執行緒一起併發執行時,同時對一個數據進行修改,就可能會造成資料的不一致性,看下面的例子:

假設一個簡單的int欄位被定義和初始化: 
int counter = 0; 
該counter欄位在兩個執行緒A和B之間共享。假設執行緒A、執行緒B同時對counter進行計算,遞增運算: 
counter ++; 
那麼計算的結果應該是 2 。但是真實的結果卻是 1 ,這是因為:執行緒A得到的運算結果是1,執行緒B的運算結果也是1,當執行緒A將結果寫回到記憶體中的 count 後,執行緒B也將結果寫回到記憶體中去,這就會把執行緒A的計算結果給覆蓋了。

上面僅僅是一種簡單的情況,還有更復雜的情況,本文不深入去了解。

2、多執行緒併發不安全的原因已經知道,那麼針對這個種情況,java中有兩種解決思路:

  1. 給共享的資源加把鎖,保證每個資源變數每時每刻至多被一個執行緒佔用。
  2. 讓執行緒也擁有資源,不用去共享程序中的資源。

3、基於上面的兩種思路,下面便是3種實施方案:

1. 多例項、或者是多副本(ThreadLocal):對應著思路2,ThreadLocal可以為每個執行緒的維護一個私有的本地變數,可參考java執行緒副本–ThreadLocal; 
2. 使用鎖機制 synchronize、lock方式:

為資源加鎖,可參考我寫的一系列文章; 
3. 使用 java.util.concurrent 下面的類庫:有JDK提供的執行緒安全的集合類


可能說的還不太清楚,更新一下,以及給出一個執行緒安全模擬的例子: 
上面說了,多執行緒之所以不安全,是因為共享著資源(如果沒有資源變數共享,那麼多執行緒一定是安全的)。比如,存在共享變數a,執行緒A在使用變數a時進行計算時,因為時間片的到來,導致執行緒不得不由執行中狀態進入就緒狀態,暫停執行。等該執行緒A又重新被排程,得以繼續執行時,得到了最終的結果。但是此時記憶體中的變數a可能已經被其他執行緒改變了,但執行緒A的結果再寫回到記憶體中時,就會覆蓋了其他執行緒的計算結果,這就是多執行緒不安全的原理。

下面給出執行緒安全模擬的例子的思路:1、讓三個執行緒瞬間同時併發(不得不用到鎖,wait/notify機制,如果不懂,只要知道這是 等待/通知 便可,下面有註釋);2、模擬3個執行緒共享著一個變數,使用變數進行計算的過程 與 將計算結果分成兩次執行。 
下面是沒有進行同步,也就是執行緒不安全的情況:

​
CountMoney countMoney = new CountMoney();
String obj="";
    //建立啟動3個執行緒
    for(int i=0;i<3;i++){
        Thread t1 = new Thread(){
            @Override
            public void run() {
            //用鎖來讓執行緒第一次執行時,進入等待狀態,直到被通知來了才繼續往下執行
                synchronized (obj) {
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //通知來了後,執行addMoney的方法
                countMoney .addMoney(1);
            }
        };
        //執行緒啟動
        t1.start();
        //確保建立的執行緒的優先順序一樣
        t1.setPriority(Thread.NORM_PRIORITY);
    }
    try {
        //確保建立的3個執行緒已經運行了一次,進入等待狀態
        Thread.sleep(200);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    synchronized (obj) {
        //瞬間喚醒3個執行緒
        obj.notifyAll();
    }

​

CountMoney 類

​
public class CountMoney {
    //執行緒共享著CountMoney物件中的money變數
    volatile long money = 0;

    public long getMoney() {
        return money;
    }

    public void setMoney(long money) {
        this.money = money;
    }

    public  void addMoney(long a) {//synchronized
        //1、取得變數money的值,計算出結果
        a = getMoney() + a;
        //執行緒完成第一步後,讓出CPU;目的是:模擬1、2兩行程式碼是分成兩次執行的,不是一次性執行的
        Thread.yield();
        //2、將計算結果寫回到變數money中
        setMoney(a);
        System.out.println("執行緒"+Thread.currentThread().getName()+"的計算結果"+getMoney());
    }

}

​

執行結果:

執行緒Thread-2的計算結果1 
執行緒Thread-1的計算結果1 
執行緒Thread-0的計算結果1

我們再來看一下,加鎖後的 addMoney()方法,也就是進行同步後:

​
public synchronized void addMoney(long a) {//加了synchronized 修飾
        //1、取得變數money的值,計算出結果
        a = getMoney() + a;
        //執行緒完成第一步後,讓出CPU;目的是:模擬1、2兩行程式碼是分成兩次執行的,不是一次性執行的
        Thread.yield();
        //2、將計算結果寫回到變數money中
        setMoney(a);
        System.out.println("執行緒"+Thread.currentThread().getName()+"的計算結果"+getMoney());
    }

​

執行結果:

執行緒Thread-2的計算結果1 
執行緒Thread-1的計算結果2 
執行緒Thread-0的計算結果3

 

鎖和同步

常用的保證Java操作原子性的工具是鎖和同步方法(或者同步程式碼塊)。使用鎖,可以保證同一時間只有一個執行緒能拿到鎖,也就保證了同一時間只有一個執行緒能執行申請鎖和釋放鎖之間的程式碼。

public void testLock () {
  lock.lock();
  try{
    int j = i;
    i = j + 1;
  } finally {
    lock.unlock();
  }
}

與鎖類似的是同步方法或者同步程式碼塊。使用非靜態同步方法時,鎖住的是當前例項;使用靜態同步方法時,鎖住的是該類的Class物件;使用靜態程式碼塊時,鎖住的是synchronized關鍵字後面括號內的物件。下面是同步程式碼塊示例

public void testLock () {
  synchronized (anyObject){
    int j = i;
    i = j + 1;
  }
}

無論使用鎖還是synchronized,本質都是一樣,通過鎖來實現資源的排它性,從而實際目的碼段同一時間只會被一個執行緒執行,進而保證了目的碼段的原子性。這是一種以犧牲效能為代價的方法。