程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> java線程簡介 -共享對數據的訪問

java線程簡介 -共享對數據的訪問

編輯:關於JAVA

共享變量

要使多個線程在一個程序中有用,它們必須有某種方法可以互相通信或共享它們的結果。

讓線程共享其結果的最簡單方法是使用共享變量。它們還應該使用同步來確保值從一個線程正確傳播到另一個線程,以及防止當一個線程正在更新一些相關數據項時,另一個線程看到不一致的中間結果。

線程基礎中計算素數的示例使用了一個共享布爾變量,用於表示指定的時間段已經過去了。這說明了在線程間共享數據最簡單的形式是:輪詢共享變量以查看另一個線程是否已經完成執行某項任務。

存在於同一個內存空間中的所有線程

正如前面討論過的,線程與進程有許多共同點,不同的是線程與同一進程中的其它線程共享相同的進程上下文,包括內存。這非常便利,但也有重大責任。只要訪問共享變量(靜態或實例字段),線程就可以方便地互相交換數據,但線程還必須確保它們以受控的方式訪問共享變量,以免它們互相干擾對方的更改。

任何線程可以訪問所有其作用域內的變量,就象主線程可以訪問該變量一樣。素數示例使用了一個公用實例字段,叫做 finished,用於表示已經過了指定的時間。當計時器過期時,一個線程會寫這個字段;另一個線程會定期讀取這個字段,以檢查它是否應該停止。注:這個字段被聲明成 volatile,這對於這個程序的正確運行非常重要。在本章的後面,我們將看到原因。

受控訪問的同步

為了確保可以在線程之間以受控方式共享數據,Java 語言提供了兩個關鍵字:synchronized 和 volatile。

Synchronized 有兩個重要含義:它確保了一次只有一個線程可以執行代碼的受保護部分(互斥,mutual exclusion 或者說 mutex),而且它確保了一個線程更改的數據對於其它線程是可見的(更改的可見性)。

如果沒有同步,數據很容易就處於不一致狀態。例如,如果一個線程正在更新兩個相關值(比如,粒子的位置和速率),而另一個線程正在讀取這兩個值,有可能在第一個線程只寫了一個值,還沒有寫另一個值的時候,調度第二個線程運行,這樣它就會看到一個舊值和一個新值。同步讓我們可以定義必須原子地運行的代碼塊,這樣對於其他線程而言,它們要麼都執行,要麼都不執行。

同步的原子執行或互斥方面類似於其它操作環境中的臨界段的概念。

確保共享數據更改的可見性

同步可以讓我們確保線程看到一致的內存視圖。

處理器可以使用高速緩存加速對內存的訪問(或者編譯器可以將值存儲到寄存器中以便進行更快的訪問)。在一些多處理器體系結構上,如果在一個處理器的高速緩存中修改了內存位置,沒有必要讓其它處理器看到這一修改,直到刷新了寫入器的高速緩存並且使讀取器的高速緩存無效。

這表示在這樣的系統上,對於同一變量,在兩個不同處理器上執行的兩個線程可能會看到兩個不同的值!這聽起來很嚇人,但它卻很常見。它只是表示在訪問其它線程使用或修改的數據時,必須遵循某些規則。

Volatile 比同步更簡單,只適合於控制對基本變量(整數、布爾變量等)的單個實例的訪問。當一個變量被聲明成 volatile,任何對該變量的寫操作都會繞過高速緩存,直接寫入主內存,而任何對該變量的讀取也都繞過高速緩存,直接取自主內存。這表示所有線程在任何時候看到的 volatile 變量值都相同。

如果沒有正確的同步,線程可能會看到舊的變量值,或者引起其它形式的數據損壞。

用鎖保護的原子代碼塊

Volatile 對於確保每個線程看到最新的變量值非常有用,但有時我們需要保護比較大的代碼片段,如涉及更新多個變量的片段。

同步使用監控器(monitor)或鎖的概念,以協調對特定代碼塊的訪問。

每個 Java 對象都有一個相關的鎖。同一時間只能有一個線程持有 Java 鎖。當線程進入 synchronized 代碼塊時,線程會阻塞並等待,直到鎖可用,當它可用時,就會獲得這個鎖,然後執行代碼塊。當控制退出受保護的代碼塊時,即到達了代碼塊末尾或者拋出了沒有在 synchronized 塊中捕獲的異常時,它就會釋放該鎖。

這樣,每次只有一個線程可以執行受給定監控器保護的代碼塊。從其它線程的角度看,該代碼塊可以看作是原子的,它要麼全部執行,要麼根本不執行。

簡單的同步示例

使用 synchronized 塊可以讓您將一組相關更新作為一個集合來執行,而不必擔心其它線程中斷或看到計算的中間結果。以下示例代碼將打印“1 0”或“0 1”。如果沒有同步,它還會打印“1 1”(或“0 0”,隨便您信不信)。

public class SyncExample {
  private static lockObject = new Object();
  private static class Thread1 extends Thread {
   public void run() {
    synchronized (lockObject) {
     x = y = 0;
     System.out.println(x);
    }
   }
  }
  private static class Thread2 extends Thread {
   public void run() {
    synchronized (lockObject) {
     x = y = 1;
     System.out.println(y);
    }
   }
  }
  public static void main(String[] args) {
   new Thread1().run();
   new Thread2().run();
  }
}

在這兩個線程中都必須使用同步,以便使這個程序正確工作。

Java 鎖定

Java 鎖定合並了一種互斥形式。每次只有一個線程可以持有鎖。鎖用於保護代碼塊或整個方法,必須記住是鎖的身份保護了代碼塊,而不是代碼塊本身,這一點很重要。一個鎖可以保護許多代碼塊或方法。

反之,僅僅因為代碼塊由鎖保護並不表示兩個線程不能同時執行該代碼塊。它只表示如果兩個線程正在等待相同的鎖,則它們不能同時執行該代碼。

在以下示例中,兩個線程可以同時不受限制地執行 setLastAccess() 中的 synchronized 塊,因為每個線程有一個不同的 thingie 值。因此,synchronized 代碼塊受到兩個正在執行的線程中不同鎖的保護。

public class SyncExample {
  public static class Thingie {
   private Date lastAccess;
   public synchronized void setLastAccess(Date date) {
    this.lastAccess = date;
   }
  }
  public static class MyThread extends Thread {
   private Thingie thingie;
   public MyThread(Thingie thingie) {
    this.thingie = thingie;
   }
   public void run() {
    thingie.setLastAccess(new Date());
   }
  }
  public static void main() {
   Thingie thingie1 = new Thingie(),
    thingie2 = new Thingie();
   new MyThread(thingie1).start();
   new MyThread(thingie2).start();
  }
}

同步的方法

創建 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 方法。

同步的塊

synchronized 塊的語法比 synchronized 方法稍微復雜一點,因為還需要顯式地指定鎖要保護哪個塊。Point 的以下版本等價於前一頁中顯示的版本:

public class Point {
  public void setXY(int x, int y) {
   synchronized (this) {
    this.x = x;
    this.y = y;
   }
  }
}

使用 this 引用作為鎖很常見,但這並不是必需的。這表示該代碼塊將與這個類中的 synchronized 方法使用同一個鎖。

由於同步防止了多個線程同時執行一個代碼塊,因此性能上就有問題,即使是在單處理器系統上。最好在盡可能最小的需要保護的代碼塊上使用同步。

訪問局部(基於堆棧的)變量從來不需要受到保護,因為它們只能被自己所屬的線程訪問。

大多數類並沒有同步

因為同步會帶來小小的性能損失,大多數通用類,如 java.util 中的 Collection 類,不在內部使用同步。這表示在沒有附加同步的情況下,不能在多個線程中使用諸如 HashMap 這樣的類。

通過每次訪問共享集合中的方法時使用同步,可以在多線程應用程序中使用 Collection 類。對於任何給定的集合,每次必須用同一個鎖進行同步。通常可以選擇集合對象本身作為鎖。

下一頁中的示例類 SimpleCache 顯示了如何使用 HashMap 以線程安全的方式提供高速緩存。但是,通常適當的同步並不只是意味著同步每個方法。

Collections 類提供了一組便利的用於 List、Map 和 Set 接口的封裝器。您可以用 Collections.synchronizedMap 封裝 Map,它將確保所有對該映射的訪問都被正確同步。

如果類的文檔沒有說明它是線程安全的,那麼您必須假設它不是。

示例:簡單的線程安全的高速緩存

如以下代碼樣本所示,SimpleCache.java 使用 HashMap 為對象裝入器提供了一個簡單的高速緩存。load() 方法知道怎樣按對象的鍵裝入對象。在一次裝入對象之後,該對象就被存儲到高速緩存中,這樣以後的訪問就會從高速緩存中檢索它,而不是每次都全部地裝入它。對共享高速緩存的每個訪問都受到 synchronized 塊保護。由於它被正確同步,所以多個線程可以同時調用 getObject 和 clearCache 方法,而沒有數據損壞的風險。

public class SimpleCache {
  private final Map cache = new HashMap();
  public Object load(String objectName) {
   // load the object somehow
  }
  public void clearCache() {
   synchronized (cache) {
    cache.clear();
   }
  }
  public Object getObject(String objectName) {
   synchronized (cache) {
    Object o = cache.get(objectName);
    if (o == null) {
     o = load(objectName);
     cache.put(objectName, o);
    }
   }
   return o;
  }
}

小結

由於線程執行的計時是不確定的,我們需要小心,以控制線程對共享數據的訪問。否則,多個並發線程會互相干擾對方的更改,從而損壞數據,或者其它線程也許不能及時看到對共享數據的更改。

通過使用同步來保護對共享變量的訪問,我們可以確保線程以可預料的方式與程序變量進行交互。

每個 Java 對象都可以充當鎖,synchronized 塊可以確保一次只有一個線程執行由給定鎖保護的 synchronized 代碼。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved