Java 應用程序中的多線程可以共享資源,例如文件、數據庫、內存等。當線程以併發模式訪問共享數據時,共享數據可能會發生衝突。Java引入線程同步的概念,以實現共享數據的一致性。線程同步機制讓多個線程有序的訪問共享資源,而不是同時操作共享資源。
1 . 同步概念
在線程異步模式的情況下,同一時刻有一個線程在修改共享數據,另一個線程在讀取共享數據,當修改共享數據的線程沒有處理完畢,讀取數據的線程肯定會得到錯誤的結果。如果採用多線程的同步控制機制,當處理共享數據的線程完成處理數據之後,讀取線程讀取數據。
通過分析多線程出售火車票的例子,可以更好得理解線程同步的概念。線程 Thread1 和線程 Thread2 都可以出售火車票,但是這個過程中會出現數據與時間信息不一致的情況。線程 Thread1 查詢數據庫,發現某張火車票 T 可以出售,所以準備出售此票;此時系統切換到線程Thread2執行,它在數據庫中查詢存票,發現上面的火車票T可以出售,所以線程Thread2將這張火車票 T 售出;當系統再次切換到線程 Thread1 執行時,它又賣出同樣的票 T。這是一個典型的由於數據不同步而導致的錯誤。
下面舉一個線程異步模式訪問數據的例子。
//文件:程序ThreadNoSynchronized.java 描述:多線程不同步的原因
class ShareData {
public static String szData = ""; // 聲明,並初始化字符串數據域,作爲共享數據
}
class ThreadDemo extends Thread {
private ShareData oShare; // 聲明,並初始化ShareData 數據域
ThreadDemo() {
} // 聲明,並實現ThreadDemo 構造方法
// 聲明,並實現ThreadDemo 帶參數的構造方法
ThreadDemo(String szName, ShareData oShare) {
super(szName); // 調用父類的構造方法
this.oShare = oShare; // 初始化oShare域
}
public void run() {
for (int i = 0; i < 5; i++) {
if (this.getName().equals("Thread1")) {
oShare.szData = "這是第 1 個線程";
// 爲了演示產生的問題,這裏設置一次睡眠
try {
Thread.sleep((int) Math.random() * 100); // 休眠
} catch (InterruptedException e) { // 捕獲異常
}
System.out.println(this.getName() + ":" + oShare.szData); // 輸出字符串信息
} else if (this.getName().equals("Thread2")) {
oShare.szData = "這是第 2 個線程";
// 爲了演示產生的問題,這裏設置一次睡眠
try {
Thread.sleep((int) Math.random() * 100); // 線程休眠
} catch (InterruptedException e) // 捕獲異常
{
}
System.out.println(this.getName() + ":" + oShare.szData); // 輸出字符串信息
}
}
}
}
public class ThreadNoSynchronized {
public static void main(String argv[]) {
ShareData oShare = new ShareData(); // 創建,初始化ShareData對象oShare
ThreadDemo th1 = new ThreadDemo("Thread1", oShare); // 創建線程th1
ThreadDemo th2 = new ThreadDemo("Thread2", oShare); // 創建線程th2
th1.start(); // 啓動線程th1
th2.start(); // 啓動線程th2
}
}
Thread1:這是第 2 個線程
Thread1:這是第 1 個線程
Thread1:這是第 1 個線程
Thread1:這是第 1 個線程
Thread1:這是第 1 個線程
Thread2:這是第 2 個線程
Thread2:這是第 2 個線程
Thread2:這是第 2 個線程
Thread2:這是第 2 個線程
Thread2:這是第 2 個線程
程序中預想的結果是:“Thead1:這是第1 個線程”或“Thead2:這是第2 個線程”,但是線程對數據的異步操作導致運行結果出現了差錯。 上面程序是由於線程不同步而導致錯誤。爲了解決此類問題,Java 提供了“鎖”機制實現線程的同步。
鎖機制的原理是每個線程進入共享代碼之前獲得鎖,否則不能進入共享代碼區,並且在退出共享代碼之前釋放該鎖,這樣就解決了多個線程競爭共享代碼的情況,達到線程同步的目的。Java中鎖機制的實現方法是共享代碼之前加入 synchronized 關鍵字。
在一個類中,用關鍵字 synchonized 聲明的方法爲同步方法。Java 有一個專門負責管理線程對象中同步方法訪問的工具——同步模型監視器,它的原理是爲每個具有同步代碼的對象準備惟一的一把“鎖”。當多個線程訪問對象時,只有取得鎖的線程才能進入同步方法,其他訪問共享對象的線程停留在對象中等待,如果獲得鎖的線程調用wait方法放棄鎖,那麼其他等待獲得鎖的線程將有機會獲得鎖。當某一個等待線程取得鎖,它將執行同步方法,而其他沒有取得鎖的線程仍然繼續等待獲得鎖。
Java 程序中線程之間通過消息實現相互通信,wait()、notify()及 notifyAll()方法可完成線程間的消息傳遞。例如,一個對象包含一個 synchonized 同步方法,同一時刻只能有一個獲得鎖的線程訪問該對象中的同步方法,其他線程被阻塞在對象中等待獲得鎖。當線程調用 wait()方法可使該線程進入阻塞狀態,其他線程調用notify()或 notifyAll()方法可以喚醒該線程。
2 .同步格式
當把一語句塊聲明爲 synchornized,在同一時間,它的訪問線程之一才能執行該語句塊。
//方法同步:用關鍵字 synchonized 可將方法聲明爲同步
class 類名{
public synchonized 類型名稱 方法名稱(){
......
}
}
// 語句塊同步: 對於同步塊,synchornized 獲取的是參數中的對象鎖。
synchornized(obj)
{
//………………….
}
當線程執行到這裏的同步塊時,它必須獲取 obj 這個對象的鎖才能執行同步塊;否則線程只能等待獲得鎖。必須注意的是obj對象的作用範圍不同,控制情況不盡相同。示例如下。
public void method()
{
Object obj= new Object(); //創建局部Object類型對象obj
synchornized(obj) //同步塊
{
//……………..
}
}
上面的代碼創建了一個局部對象obj。由於每一個線程執行到 Object obj = new Object()時都會產生一個 obj 對象,每一個線程都可以獲得創建的新的 obj對象的鎖,不會相互影響,因此這段程序不會起到同步作用。
3)同步類的屬性:如果同步的是類的屬性,情況就不同了。同步類的成員變量的一般格式如下。
class method
{
Object o = new Object(); //創建Object類型的成員變量o
public void test()
{
synchornized(o) //同步塊
{
//………………………
}
}
}
當兩個併發線程訪問同一個對象的 synchornized(o)同步代碼塊時,一段時間內只能有一個線程運行。另外的線程必須等到當前線程執行完同步代碼塊釋放鎖之後,獲得鎖的線程將執行同步代碼塊。
有時可以通過下面的格式聲明同步塊。
public void method()
{
synchornized(this) //同步塊
{
//………………………
}
}
當有一個線程訪問某個對象的 synchornized(this)同步代碼塊時,另外一個線程必須等待該線程執行完此代碼塊,其他線程可以訪問該對象中的非 synchornized(this)同步代碼。如果類中包含多個 synchornized(this)同步代碼塊,如果同步線程有一個訪問其中一個代碼塊,則其他線程不能訪問該對象的所有 synchornized(this)同步代碼塊。對於下面形式的同步塊而言,調用 ClassName 對象實例的並行線程中只有一個線程能夠訪問該對象。
synchornized(ClassName.class)
{
//…………………….
}