1. 程式人生 > >Java執行緒:執行緒的同步問題

Java執行緒:執行緒的同步問題

一、同步問題提出

執行緒的同步是為了防止多個執行緒訪問一個數據物件時,對資料造成的破壞。
例如:兩個執行緒ThreadA、ThreadB都操作同一個物件Foo物件,並修改Foo物件上的資料。
  1. public class Foo {
  2.     private int x = 100;
  3.     public int getX() {
  4.         return x;
  5.     }
  6.     public int fix(int y) {
  7.         x = x – y;
  8.         return x;
  9.     }
  10. }
  11. public class MyRunnable implements Runnable {
  12.     private Foo foo = new Foo();
  13.     public static void main(String[] args) {
  14.         MyRunnable r = new MyRunnable();
  15.         Thread ta = new Thread(r, “Thread-A”);
  16.         Thread tb = new Thread(r, “Thread-B”);
  17.         ta.start();
  18.         tb.start();
  19.     }
  20.     public void run() {
  21.         for (int i = 0; i < 3; i++) {
  22.             this.fix(30);
  23.             try {
  24.                 Thread.sleep(1);
  25.             } catch (InterruptedException e) {
  26.                 e.printStackTrace();
  27.             }
  28.             System.out.println(Thread.currentThread().getName() + ” : 當前foo物件的x值= “ + foo.getX());
  29.         }
  30.     }
  31.     public int fix(int y) {
  32.         return foo.fix(y);
  33.     }
  34. }
複製程式碼 執行結果:
Thread-A : 當前foo物件的x值= 40
Thread-B : 當前foo物件的x值= 40
Thread-B : 當前foo物件的x值= -20
Thread-A : 當前foo物件的x值= -50
Thread-A : 當前foo物件的x值= -80
Thread-B : 當前foo物件的x值= -80
Process finished with exit code 0


從結果發現,這樣的輸出值明顯是不合理的。原因是兩個執行緒不加控制的訪問Foo物件並修改其資料所致。

如果要保持結果的合理性,只需要達到一個目的,就是將對Foo的訪問加以限制,每次只能有一個執行緒在訪問。這樣就能保證Foo物件中資料的合理性了。

在具體的Java程式碼中需要完成一下兩個操作:
把競爭訪問的資源類Foo變數x標識為private;
同步哪些修改變數的程式碼,使用synchronized關鍵字同步方法或程式碼。

二、同步和鎖定

1、鎖的原理

Java中每個物件都有一個內建鎖

當程式執行到非靜態的synchronized同步方法上時,自動獲得與正在執行程式碼類的當前例項(this例項)有關的鎖。獲得一個物件的鎖也稱為獲取鎖、鎖定物件、在物件上鎖定或在物件上同步。

當程式執行到synchronized同步方法或程式碼塊時才該物件鎖才起作用。

一個物件只有一個鎖。所以,如果一個執行緒獲得該鎖,就沒有其他執行緒可以獲得鎖,直到第一個執行緒釋放(或返回)鎖。這也意味著任何其他執行緒都不能進入該物件上的synchronized方法或程式碼塊,直到該鎖被釋放。

釋放鎖是指持鎖執行緒退出了synchronized同步方法或程式碼塊。

關於鎖和同步,有一下幾個要點:
1)、只能同步方法,而不能同步變數和類;
2)、每個物件只有一個鎖;當提到同步時,應該清楚在什麼上同步?也就是說,在哪個物件上同步?
3)、不必同步類中所有的方法,類可以同時擁有同步和非同步方法。
4)、如果兩個執行緒要執行一個類中的synchronized方法,並且兩個執行緒使用相同的例項來呼叫方法,那麼一次只能有一個執行緒能夠執行方法,另一個需要等待,直到鎖被釋放。也就是說:如果一個執行緒在物件上獲得一個鎖,就沒有任何其他執行緒可以進入(該物件的)類中的任何一個同步方法。
5)、如果執行緒擁有同步和非同步方法,則非同步方法可以被多個執行緒自由訪問而不受鎖的限制。
6)、執行緒睡眠時,它所持的任何鎖都不會釋放。

7)、執行緒可以獲得多個鎖。比如,在一個物件的同步方法裡面呼叫另外一個物件的同步方法,則獲取了兩個物件的同步鎖。
8)、同步損害併發性,應該儘可能縮小同步範圍。同步不但可以同步整個方法,還可以同步方法中一部分程式碼塊。
9)、在使用同步程式碼塊時候,應該指定在哪個物件上同步,也就是說要獲取哪個物件的鎖。例如:
    public int fix(int y) {
        synchronized (this) {
            x = x – y;
        }
        return x;
    }

當然,同步方法也可以改寫為非同步方法,但功能完全一樣的,例如:
    public synchronized int getX() {
        return x++;
    }

    public int getX() {
        synchronized (this) {
            return x;
        }
    }
效果是完全一樣的。

三、靜態方法同步

要同步靜態方法,需要一個用於整個類物件的鎖,這個物件是就是這個類(XXX.class)。
例如:
public static synchronized int setName(String name){
      Xxx.name = name;
}
等價於
public static int setName(String name){
      synchronized(Xxx.class){
            Xxx.name = name;
      }
}

四、如果執行緒不能不能獲得鎖會怎麼樣

如果執行緒試圖進入同步方法,而其鎖已經被佔用,則執行緒在該物件上被阻塞。實質上,執行緒進入該物件的的一種池中,必須在哪裡等待,直到其鎖被釋放,該執行緒再次變為可執行或執行為止。

當考慮阻塞時,一定要注意哪個物件正被用於鎖定:
1、呼叫同一個物件中非靜態同步方法的執行緒將彼此阻塞。如果是不同物件,則每個執行緒有自己的物件的鎖,執行緒間彼此互不干預。

2、呼叫同一個類中的靜態同步方法的執行緒將彼此阻塞,它們都是鎖定在相同的Class物件上。

3、靜態同步方法和非靜態同步方法將永遠不會彼此阻塞,因為靜態方法鎖定在Class物件上,非靜態方法鎖定在該類的物件上。

4、對於同步程式碼塊,要看清楚什麼物件已經用於鎖定(synchronized後面括號的內容)。在同一個物件上進行同步的執行緒將彼此阻塞,在不同物件上鎖定的執行緒將永遠不會彼此阻塞。