線程調度和執行的計時是不確定的。如果兩個線程同時運行,而且都不等待,您必須假設在任何兩個指令之間,其它線程都可以運行並修改程序變量。
不僅不同機器之間的結果可能不同,而且在同一機器上多次運行同一程序也可能生成不同結果。永遠不要假設一個線程會在另一個線程之前執行某些操作
以下示例使用兩個線程,一個用於計時,一個用於執行實際工作。主線程使用非常簡單的算法計算素數。
在它啓動之前,它創建並啓動一個計時器線程,這個線程會休眠十秒鐘,然後設置一個主線程要檢查的標誌。十秒鐘之後,主線程將停止。請注意,共享標誌被聲明成 volatile
。
/**
* CalculatePrimes -- calculate as many primes as we can in ten seconds
*/
public class CalculatePrimes extends Thread {
public static final int MAX_PRIMES = 1000000;
public static final int TEN_SECONDS = 10000;
public volatile boolean finished = false;
public void run() {
int[] primes = new int[MAX_PRIMES];
int count = 0;
for (int i=2; count<MAX_PRIMES; i++) {
// Check to see if the timer has expired
if (finished) {
break;
}
boolean prime = true;
for (int j=0; j<count; j++) {
if (i % primes[j] == 0) {
prime = false;
break;
}
}
if (prime) {
primes[count++] = i;
System.out.println("Found prime: " + i);
}
}
}
public static void main(String[] args) {
CalculatePrimes calculator = new CalculatePrimes();
calculator.start();
try {
Thread.sleep(TEN_SECONDS);
}
catch (InterruptedException e) {
// fall through
}
calculator.finished = true;
}
}
線程與進程有許多共同點,不同的是線程與同一進程中的其它線程共享相同的進程上下文,包括內存。這非常便利,但也有重大責任。只要訪問共享變量(靜態或實例字段),線程就可以方便地互相交換數據,但線程還必須確保它們以受控的方式訪問共享變量,以免它們互相干擾對方的更改。 任何線程可以訪問所有其作用域內的變量,就象主線程可以訪問該變量一樣。素數示例使用了一個公用實例字段,叫做finished用於表示已經過了指定的時間。當計時器過期時,一個線程會寫這個字段;另一個線程會定期讀取這個字段,以檢查它是否應該停止。注:這個字段被聲明成 volatile,這對於這個程序的正確運行非常重要。
爲了確保可以在線程之間以受控方式共享數據,Java 語言提供了兩個關鍵字:synchronized 和 volatile。
Synchronized 有兩個重要含義:它確保了一次只有一個線程可以執行代碼的受保護部分(互斥,mutual exclusion 或者說 mutex)
,而且它確保了一個線程更改的數據對於其它線程是可見的(更改的可見性)。
如果沒有同步,數據很容易就處於不一致狀態。例如,如果一個線程正在更新兩個相關值(比如,粒子的位置和速率),而另一個線程正在
讀取這兩個值,有可能在第一個線程只寫了一個值,還沒有寫另一個值的時候,調度第二個線程運行,這樣它就會看到一箇舊值和一個新值。
同步讓我們可以定義必須原子地運行的代碼塊,這樣對於其他線程而言,它們要麼都執行,要麼都不執行。
同步的原子執行或互斥方面類似於其它操作環境中的臨界段的概念。
Volatile比同步更簡單,只適合於控制對基本變量(整數、布爾變量等)的單個實例的訪問。當一個變量被聲明成 volatile任何
對該變量的寫操作都會繞過高速緩存,直接寫入主內存,而任何對該變量的讀取也都繞過高速緩存,直接取自主內存。這表示所有線程在任
何時候看到的 volatile 變量值都相同。
如果沒有正確的同步,線程可能會看到舊的變量值,或者引起其它形式的數據損壞。
Volatile
對於確保每個線程看到最新的變量值非常有用,但有時我們需要保護比較大的代碼片段,如涉及更新多個變量的片段。
同步使用監控器(monitor)或鎖的概念,以協調對特定代碼塊的訪問。
每個 Java 對象都有一個相關的鎖。同一時間只能有一個線程持有 Java 鎖。當線程進入 synchronized代碼塊時,線程會阻塞並等待,直到鎖可用,當它可用時,就會獲得這個鎖,然後執行代碼塊。當控制退出受保護的代碼塊時,即到達了代碼塊末尾或者拋出了沒有在 synchronized塊中捕獲的異常時,它就會釋放該鎖。
這樣,每次只有一個線程可以執行受給定監控器保護的代碼塊。從其它線程的角度看,該代碼塊可以看作是原子的,它要麼全部執行,要麼
根本不執行。
Java 鎖定合併了一種互斥形式。每次只有一個線程可以持有鎖。鎖用於保護代碼塊或整個方法,必須記住是鎖的身份保護了代碼塊,而不是代碼塊本身,這一點很重要。一個鎖可以保護許多代碼塊或方法。
反之,僅僅因爲代碼塊由鎖保護並不表示兩個線程不能同時執行該代碼塊。它只表示如果兩個線程正在等待相同的鎖,則它們不能同時執行該代碼。
創建synchronized塊的最簡單方法是將方法聲明成 synchronized這表示在進入方法主體之前,調用者必須獲得鎖:
public class Point {
public synchronized void setXY(int x, int y) {
this.x = x;
this.y = y;
}
}
對於普通的 synchronized
方法,這個鎖是一個對象,將針對它調用方法。對於靜態 synchronized
方法,這個鎖是與 Class
對象相關的監控器,在該對象中聲明瞭方法。
僅僅因爲 setXY()
被聲明成 synchronized
並不表示兩個不同的線程不能同時執行 setXY()
,只要它們調用不同的 Point 實例的 setXY()就可同時執行。Point 實例,一次只能有一個線程執行 setXY(),或 Point
的任何其它 synchronized
方法。