執行緒所操作的可執行(Runnable)物件,就相當於JVM中加裝的虛擬CPU所執行的程式碼。如果有兩個執行緒試圖執行的程式碼有所重疊,就要小心所重疊程式碼中共用的資料部份,以免共用存取時,發生資料的競速(Race condition)問題。
舉個例子來說,如果你開發一個簡單的Stack:
public class Stack {
private int[] data;
private int index;
public Stack(int capacity) {
data = new int[capacity];
}
public void put(int d) {
data[index] = d;
index++;
}
public int pop() {
index--;
return data[index];
}
}
private int[] data;
private int index;
public Stack(int capacity) {
data = new int[capacity];
}
public void put(int d) {
data[index] = d;
index++;
}
public int pop() {
index--;
return data[index];
}
}
index指向陣列中可以儲存資料的索引位置。這個程式使用在單執行緒時沒有問題,但如果使用在多執行緒下,例如某個可執行物件的run()中執行了put()方法:
public class Some implements Runnable {
private Stack stack;
...
public void run() {
....
stack.put(d);
...
}
}
private Stack stack;
...
public void run() {
....
stack.put(d);
...
}
}
假設index原先是2,當執行緒執行put()的第一行時,原本下一步該進行遞增index的動作(也就是變成3),不過此時正巧另一個執行緒執行了pop():
public class Other implements Runnable {
private Stack stack;
...
public void run() {
....
int p = stack.pop();
...
}
}
private Stack stack;
...
public void run() {
....
int p = stack.pop();
...
}
}
當執行緒執行了pop()的第一行遞減index後(變成1了),又回到先前執行緒的執行,此時又遞增了index(變成2了),所以這個時候,原本該遞增的index沒有遞增(原來應該變成3),為錯誤的結果。
錯 誤發生的原因在於,index為兩個執行緒共用,因而在某些時機點,發生競爭存取的情況,使得原來該在一個方法中完成的單元操作(陣列值指定後,索引也必 須被遞增,這兩個動作不可分割)被另一個執行緒介入。若要解決這個問題,可以將必須由一個執行緒完成的單元操作程式碼鎖定,完成操作後再開放給其它執行緒 存取共用的資料。
你可以使用synchronized關鍵字,標示執行緒所必須完成的單元操作程式碼範圍,例如最基本的,就是標示方法為synchronized:
public class Stack {
private int[] data;
private int index;
public Stack(int capacity) {
data = new int[capacity];
}
public synchronized void put(int d) {
data[index] = d;
index++;
}
public synchronized int pop() {
index--;
return data[index];
}
}
private int[] data;
private int index;
public Stack(int capacity) {
data = new int[capacity];
}
public synchronized void put(int d) {
data[index] = d;
index++;
}
public synchronized int pop() {
index--;
return data[index];
}
}
每個物件內部都只會有一把鎖定旗標(Lock flag),執行緒要進入synchronized所標示的程式碼範圍,都必須取得某物件的鎖定旗標。以上例而言,在Stack的方法上標示synchronized(此時稱為synchronized method),表示進入synchronized時,必須取得Stack物件的鎖定旗標(對synchronized metho來說,預設就是取得目前物件的鎖定),若此時另一個執行緒也打算執行標示synchronized的方法,由於鎖定旗標已經被取走了,就只能等待。
只有當原先取得鎖定旗標的執行緒執行完syhchronized區域後將鎖定旗標歸還物件後,另一個執行緒才有可能取得鎖定旗標而執行所想要進入的synchronized區域。
所以若是先前的情況,有一個執行緒執行put()方法時,另一個執行緒就不能執行get()方法,所以put()方法內容一定會全執行完,也就不會發生先前競爭存取而引發的錯誤結果。
如果你確定共用存取的範圍,並不一定要將整個方法都標示為synchronized範圍,而可以如下(這是synchronized statement):
public void put(int d) {
...
synchronized(this) {
data[index] = d;
index++;
}
...
}
public int pop() {
...
synchronized(this) {
index--;
return data[index];
}
...
}
括號中指定鎖定旗標的物件來源。由於標示為synchronized範圍,一次只允許一個執行緒,所以其它的執行緒等待可能會引發效能問題,減少不必要的synchronized範圍,可以減少不必要的等待,降低效能負擔。
使用synchronized statement,可以作到更細部的控制,像是可以提供不同的物件作為鎖定旗標來源:
public class Material {
private int data1 = 0;
private int data2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void doSome() {
...
synchronized(lock1) {
...
data1++;
...
}
...
}
public void doOther() {
...
synchronized(lock2) {
...
data2--;
...
}
...
}
}
private int data1 = 0;
private int data2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void doSome() {
...
synchronized(lock1) {
...
data1++;
...
}
...
}
public void doOther() {
...
synchronized(lock2) {
...
data2--;
...
}
...
}
}
在這邊所要避免的是在doSome()中,同時被兩個以上的執行緒存取synchronized區塊,或是doOther ()中,同時被兩個以上的執行緒存取synchronized區塊,但data1與data2並不同時出現在兩個方法中,所以有個執行緒執行doSome ()而另一個執行緒執行doOther()時,並不會引發共用存取問題,此時分別提供不同的物件作為鎖定來源,就不會導致doSome()中 synchronize被一個執行緒存取時,doOther()中synchronized被另一個試圖存取時,所引發的等待延遲。