1. 程式人生 > >Java並發/多線程系列——線程安全篇(1)

Java並發/多線程系列——線程安全篇(1)

dna critical ron 查看 end 錯誤 完成 cor 數據

創建和啟動Java線程

Java線程是個對象,和其他任何的Java對象一樣。線程是類的實例java.lang.Thread,或該類的子類的實例。除了對象之外,java線程還可以執行代碼。

創建和啟動線程

在Java中創建一個線程是這樣完成的:

 Thread thread = new Thread();

要啟動Java線程,您將調用其start()方法,如下所示:

thread.start();

此示例不指定要執行的線程的任何代碼。啟動後,線程將立即停止。

有兩種方法來指定線程應該執行什麽代碼。第一個是繼承Thread類並覆蓋run()方法。第二種方法是實現Runnablejava.lang.Runnable

Thread構造函數)的接口,這兩個方法都在下面。

繼承Thread

創建線程的第一種方法是創建Thread的子類並覆蓋該run()方法。當執行start()方法後,會另起一個線程調用run()方法以下是創建Java Thread子類的示例

public class MyThread extends Thread {

  public void run(){
     System.out.println("MyThread running");
  }
}

啟動線程:

MyThread myThread = new MyThread();
myTread.start();

start()一旦線程啟動,調用將返回。它不會等到run()方法完成。run()方法將像執行不同的CPU一樣執行。run()方法執行時,它將打印出文本“MyThread running”。

你也可以創建一個這樣的匿名子類的Thread

Thread thread = new Thread(){
  public void run(){
    System.out.println("Thread Running");
  }
}

thread.start();

此示例將打印出文本“Thread running”

實現Runnable接口

創建線程的第二種方法是創建一個

java.lang.Runnable接口的實現類該實現類可以通過一個被執行Thread運行

示例:

public class MyRunnable implements Runnable {

  public void run(){
     System.out.println("MyRunnable running");
  }
}

要執行run()方法,需要創建擁有MyRunnable實例的Thread對象,如下:

Thread thread = new Thread(new MyRunnable());
thread.start();

當線程啟動時,它將調用MyRunnablerun()方法上面的例子將打印出文本“MyRunnable running”。

您還可以創建一個匿名實現Runnable,像這樣:

Runnable myRunnable = new Runnable(){

   public void run(){
      System.out.println("Runnable running");
   }
 }

Thread thread = new Thread(myRunnable);
thread.start();

繼承Thread父類還是實現Runnable接口?

這兩種方法沒有說哪一種是最好的,這兩種方法都有效。我個人而言,我更喜歡使用Runnable,並將實現的一個實例移交給一個Thread實例。Runnable通過線程池執行該操作時Runnable 實例很容易列入隊列中,直到來自池的線程空閑時再運行run()方法。而Thread的子類就難於實現

有時你可能需要實現Runnable和子類Thread例如,創建一個子類Thread可以執行多個Runnable實現線程池時通常是這種情況。

常見的陷阱:調用run()而不是start()

當創建和啟動一個線程時,一個常見的錯誤是調用run()方法而不是Threadstart(),像這樣:

Thread newThread = new Thread(MyRunnable());
newThread.run(); //應該是start();

起初你可能不會註意到這樣會發生錯誤,因為它Runnablerun()方法是像你預期的那樣執行。但是,它不是剛剛創建的新線程執行。相反,該run()方法由創建線程的線程執行。換句話說,執行上述兩行代碼的線程。由新創建的線程去調用MyRunnable實例的run()方法,你必須通過newThread.start()去調用。

線程名稱

創建Java線程時,可以給它一個名稱。該名稱可以幫助您區分不同的線程。例如,如果多個線程寫入System.out,它可以方便地查看哪個線程寫了文本。兩種不同的創建線程方式的例子:

Thread thread = new Thread("New Thread"){
    public void run(){
      System.out.println("run by:" + getName());
   }
};


thread.start();
System.out.println(thread.getName());

MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable, "New Thread");

thread.start();
System.out.println(thread.getName());

但是請註意,由於MyRunnable類不是 Thread的子類,所以它無法通過執行getName()去獲取線程名字

獲取當前線程

Thread.currentThread()方法能夠返回當前線程的實例,這樣你就可以獲取到當前線程中你想得到的東西。例如,您可以獲取當前執行代碼的線程的名稱,如下所示:

Thread thread = Thread.currentThread();
String threadName = Thread.currentThread().getName();

Java Thread示例

這是一個小例子。首先打印執行該main()方法的線程的名稱該線程由JVM分配。然後它啟動10個線程,並給它們全部一個數字作為name("" + i)然後每個線程將其名稱輸出,然後停止執行。

public class ThreadExample {
    
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
        for(int i = 0; i <10; i ++){
          new Thread("" + i){
            public void run(){
              System.out.println("Thread:" + getName() +"running");
            }
          }.start();
        }
    }

}

請註意,即使線程按順序(1,2,3...)啟動,它們可能不會按順序執行,這意味著線程0可能不是第一個用System.out把線程名稱輸出的。這是因為線程原則上是並行執行而不是順序執行的。JVM操作系統決定執行線程的順序。每次運行結果會不相同,因此這個順序不一定是他們的執行順序。

競爭條件(Race Conditions)和臨界區(Critical Sections)

競爭條件是在臨界區內可能出現的一種特殊情況。臨界區是一種輕量級機制,在某一時間內只允許一個線程執行某個給定代碼段

當多線程在臨界區執行時,執行結果可能會根據線程執行的順序而有所不同,臨界區被稱為包含競爭條件。競爭條件一詞來自比喻,即線程正在通過臨界區時進行賽跑,而競爭的結果影響了執行臨界區的結果。

這可能聽起來有點復雜,所以我將在以下部分詳細闡述競爭條件和臨界區。

臨界區

在同一應用程序中運行多個線程本身不會導致問題。當多個線程訪問相同的資源時,就會出現問題。例如多個線程同時訪問相同的內存(變量,數組或對象),系統(數據庫,Web服務等)或文件。

事實上,只有一個或多個線程寫入這些資源時才會出現問題。只要資源不變,可以安全地讓多個線程讀取相同的資源。

以下是一個臨界區的代碼示例,如果多個線程同時執行,則可能會失敗:

public class Counter {

   protected long count = 0;

   public void add(long value){
       this.count = this.count + value;
   }
}

想象一下,如果兩個線程A和B正在同一個Counter類的實例上執行add方法沒有辦法知道操作系統何時在兩個線程之間切換。add()方法中的代碼不會作為Java虛擬機的單個原子指令執行。相反,它作為一組較小的指令執行,類似於此:

  1. 把這個記錄從內存讀入註冊表。
  2. 添加值進行註冊。
  3. 寫入寄存器到內存

觀察以下的線程A和B的混合執行會發生什麽:

 this.count = 0;

A:把這個記錄讀入一個寄存器(0)
B:將此記錄讀入註冊表(0)
B:添加值2進行註冊
B:將寄存器值(2)寫入內存。this.count現在等於2
A:添加值3進行註冊
A:將寄存器值(3)寫入內存。this.count現在等於3

兩個線程想要將值2和3添加到計數器。因此,兩個線程完成執行後的值應該是5。然而,由於兩個線程同時執行,所以結果會有所不同。

在上面列出的執行順序示例中,兩個線程從內存中讀取值0。然後,他們將它們的個人值2和3添加到值中,並將結果寫回內存。而不是5,剩下的值 this.count將是最後一個線程寫入其值的值。在上面的情況下,它是線程A,但也可能是線程B.

臨界區的競爭條件

上例中的add()方法就包含臨界區,當多個線程執行此臨界區時,會發生競爭條件。

多個線程競爭相同資源時,其中訪問資源的順序是重要的,稱為競爭條件。導致競爭條件的代碼部分稱為臨界區。

防止競爭條件

為了防止發生競爭條件,您必須確保臨界區作為原子命令執行。這意味著一旦一個線程正在執行它,就不能有其他線程可以執行它,直到第一個線程離開臨界區。

臨界區的競爭條件可以通過適當的線程同步來避免。可以使用Java代碼的同步塊來實現線程同步。線程同步也可以使用其他同步結構(如鎖或原子變量,如java.util.concurrent.atomic.AtomicInteger)來實現。

public class TwoSums {
    
    private int sum1 = 0;
    private int sum2 = 0;
    
    public void add(int val1, int val2){
        synchronized(this){
            this.sum1 += val1;   
            this.sum2 += val2;
        }
    }
}

然而,由於兩個和變量是相互獨立的,所以您可以將它們的求和分解為兩個單獨的同步塊,如下所示:

public class TwoSums {
    
    private int sum1 = 0;
    private int sum2 = 0;

    private Integer sum1Lock = new Integer(1);
    private Integer sum2Lock = new Integer(2);

    public void add(int val1, int val2){
        synchronized(this.sum1Lock){
            this.sum1 += val1;   
        }
        synchronized(this.sum2Lock){
            this.sum2 += val2;
        }
    }
}

現在兩個線程可以同時執行該add()方法。兩個同步塊在不同的對象上同步,因此兩個不同的線程可以獨立執行兩個塊。這樣線程將就有較少的等待去執行add()方法。

這個例子當然很簡單。在現實生活中的共享資源中,臨界區的分解可能會更復雜一些,並且需要更多的分析執行順序的可能性。

線程安全和共享資源

多線程同時安全地調用被稱為線程安全如果一段代碼是線程安全的,那麽它不包含任何競爭條件競爭條件僅在多個線程更新共享資源時發生。因此,重要的是要知道什麽共享資源會被多線程同時執行。

局部變量

局部變量存儲在每個線程自己的堆棧中。這意味著局部變量從不在線程之間共享。這也意味著所有本地變量基本上都是線程安全的。以下是本地變量的線程安全的示例:

public void someMethod(){

  long threadSafeInt = 0;

  threadSafeInt++;
}

本地對象的引用

引用本身不是共享的。但是,引用的對象不存儲在每個線程的本地堆棧中,所有對象都存儲在共享堆中。

如果本地創建的對象永遠不會通過創建他的方法返回,那麽它是線程安全的。實際上,只要沒有讓對象在方法之間傳遞後用於其他線程。

這是一個線程安全的本地對象的示例:

public void someMethod(){

  LocalObject localObject = new LocalObject();

  localObject.callMethod();
  method2(localObject);
}

public void method2(LocalObject localObject){
  localObject.setValue("value");
}

上面這個例子,someMethod()這個方法沒有將LocalObject傳遞出去,而是每個線程調用someMethod()都會創建一個新的LocalObject,並在自己的方法內部消化,所以這裏是線程安全的。

對象成員變量

對象成員變量與對象一起存儲在堆上。因此,如果兩個線程調用同一對象實例上的方法,並且此方法更新該對象的成員變量,則該方法是線程不安全的。這是一個線程不安全的例子:

public class NotThreadSafe{
    StringBuilder builder = new StringBuilder();

    public add(String text){
        this.builder.append(text);
    }
}

如果兩個線程在同一個NotThreadSafe實例上同時調用add()方法那麽它會導致競爭條件。例如:

NotThreadSafe sharedInstance = new NotThreadSafe();

new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();

public class MyRunnable implements Runnable{
  NotThreadSafe instance = null;

  public MyRunnable(NotThreadSafe instance){
    this.instance = instance;
  }

  public void run(){
    this.instance.add("some text");
  }
}

但是,如果兩個線程在不同的實例上同時調用add()方法 那麽它們不會產生競爭條件。把上面的例子稍加修改:

new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();

現在這兩個線程都擁有自己的實例對象,所以他們調用add方法時不會互相幹擾。代碼沒有競爭條件了。所以即使一個對象是線程不安全的,它仍然可以以不會導致競爭條件的方式運行。

線程控制逃離準則(The Thread Control Escape Rule)

為了確定你的代碼對某個資源的訪問是否是線程安全的,您可以使用“線程控制逃離準則”:

如果一個資源的創建、使用和回收都在同一個線程內完成的,並且從來沒有逃離這個線程的控制域,那麽該資源就是線程安全的

If a resource is created, used and disposed within the control of the same thread, and never escapes the control of this thread, the use of that resource is thread safe.

資源可以是任何形式的共享資源,如對象,數組,文件,數據庫連接,套接字等。在Java中,你並不總是明確地回收某個對象,因此“回收”意味著對該對象的引用不再使用或者置為 null。

即使使用線程安全的對象,如果該對象指向一個共享資源,如文件或數據庫,那麽整個應用程序可能不是線程安全的。例如,如果線程1和線程2都創建自己的數據庫連接,連接1和連接2,則使用每個連接本身是線程安全的。但是使用數據庫的連接點可能不是線程安全的。例如,如果兩個線程執行這樣的代碼:

check if record X exists
if not, insert record X

如果兩個線程同時執行,並且他們正在檢查的記錄X恰好是相同的記錄,那麽就存在兩個線程都進行插入的動作。那麽這就是線程不安全的。

這種情況也可能發生在對文件或者其他共享資源的操作上。因此,一定要區分一個線程所控制的對象到底是資源本身還是指向資源的一個引用

Java並發/多線程系列——線程安全篇(1)