複製代碼代碼如下:
public synchronized void run()
{
}
從上面的程式碼可以看出,只要在void和public之間加上synchronized關鍵字,就可以讓run方法同步,也就是說,對於同一個Java類別的物件實例,run方法同時只能被一個執行緒調用,並且目前的run執行完後,才能被其他的執行緒呼叫。即使當前執行緒執行到了run方法中的yield方法,也只是暫停了一下。由於其他執行緒無法執行run方法,因此,最終還是會由目前的執行緒來繼續執行。先看看下面的程式碼:
sychronized關鍵字只和一個物件實例綁定
複製代碼代碼如下:
class Test
{
public synchronized void method()
{
}
}
public class Sync implements Runnable
{
private Test test;
public void run()
{
test.method();
}
public Sync(Test test)
{
this.test = test;
}
public static void main(String[] args) throws Exception
{
Test test1 = new Test();
Test test2 = new Test();
Sync sync1 = new Sync(test1);
Sync sync2 = new Sync(test2);
new Thread(sync1).start();
new Thread(sync2).start();
}
}
在Test類別中的method方法是同步的。但上面的程式碼建立了兩個Test類別的實例,因此,test1和test2的method方法是分別執行的。要讓method同步,必須在建立Sync類別的實例時向它的建構方法中傳入同一個Test類別的實例,如下面的程式碼所示:
Sync sync1 = new Sync(test1);
不僅可以使用synchronized來同步非靜態方法,也可以使用synchronized來同步靜態方法。如可以如下定義method方法:
複製代碼代碼如下:
class Test
{
public static synchronized void method() { }
}
建立Test類別的物件實例如下:
Test test = new Test();
對於靜態方法來說,只要加上了synchronized關鍵字,這個方法就是同步的,無論是使用test.method(),還是使用Test.method()來呼叫method方法,method都是同步的,並不存在非靜態方法的多個實例的問題。
在23種設計模式中的單件(Singleton)模式如果按傳統的方法設計,也是線程不安全的,下面的程式碼是一個線程不安全的單件模式。
複製代碼代碼如下:
package test;
// 執行緒安全的Singleton模式
class Singleton
{
private static Singleton sample;
private Singleton()
{
}
public static Singleton getInstance()
{
if (sample == null)
{
Thread.yield(); // 為了放大Singleton模式的執行緒不安全性
sample = new Singleton();
}
return sample;
}
}
public class MyThread extends Thread
{
public void run()
{
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());
}
public static void main(String[] args)
{
Thread threads[] = new Thread[5];
for (int i = 0; i < threads.length; i++)
threads[i] = new MyThread();
for (int i = 0; i < threads.length; i++)
threads[i].start();
}
}
在上面的程式碼呼叫yield方法是為了使單件模式的執行緒不安全性表現出來,如果將這行去掉,上面的實作仍然是執行緒不安全的,只是出現的可能性小得多。
程式的運行結果如下:
複製代碼代碼如下:
25358555
26399554
7051261
29855319
5383406
上面的運行結果可能在不同的運行環境上有所有同,但一般這五行輸出不會完全相同。從這個輸出結果可以看出,透過getInstance方法得到的物件實例是五個,而不是我們期望的一個。這是因為當一個執行緒執行了Thread.yield()後,就將CPU資源交給了另一個執行緒。由於在執行緒之間切換時並未執行到建立Singleton物件實例的語句,因此,這幾個執行緒都通過了if判斷,所以,就會產生了建立五個物件實例的情況(可能創建的是四個或三個物件實例,這取決於有多少個執行緒在建立Singleton物件之前通過了if判斷,每次執行時可能結果會不一樣)。
要讓上面的單件模式變成線程安全的,只要為getInstance加上synchronized關鍵字即可。程式碼如下:
public static synchronized Singleton getInstance() { }
當然,還有更簡單的方法,就是在定義Singleton變數時就建立Singleton對象,程式碼如下:
private static final Singleton sample = new Singleton();
然後在getInstance方法中直接將sample回傳即可。這種方式雖然簡單,但不知在getInstance方法中建立Singleton物件靈活。讀者可以根據具體的需求選擇使用不同的方法來實現單件模式。
使用synchronized關鍵字時有以下四點要注意:
1. synchronized關鍵字不能繼承。
雖然可以使用synchronized來定義方法,但synchronized並不屬於方法定義的一部分,因此,synchronized關鍵字不能被繼承。如果在父類別中的某個方法使用了synchronized關鍵字,而在子類別中覆寫了這個方法,在子類別中的這個方法預設情況下並不是同步的,而必須顯式地在子類別中的這個方法加上synchronized關鍵字才可以。當然,還可以在子類別方法中呼叫父類別中對應的方法,這樣雖然子類別中的方法不是同步的,但子類別呼叫了父類別的同步方法,因此,子類別的方法也相當於同步了。這兩種方式的例子程式碼如下:
在子類別方法中加上synchronized關鍵字
複製代碼代碼如下:
class Parent
{
public synchronized void method() { }
}
class Child extends Parent
{
public synchronized void method() { }
}
在子類別方法中呼叫父類別的同步方法
複製代碼代碼如下:
class Parent
{
public synchronized void method() { }
}
class Child extends Parent
{
public void method() { super.method(); }
}
2. 定義介面方法時不能使用synchronized關鍵字。
3. 構造方法不能使用synchronized關鍵字,但可以使用下節要討論的synchronized區塊來進行同步。
4. synchronized可以自由放置。
在前面的例子中使用都是將synchronized關鍵字放在方法的回傳類型前面。但這並不是synchronized可放置唯一位置。在非靜態方法中,synchronized也可以放在方法定義的最前面,在靜態方法中,synchronized可以放在static的前面,程式碼如下:
複製代碼代碼如下:
public synchronized void method();
synchronized public void method();
public static synchronized void method();
public synchronized static void method();
synchronized public static void method();
但要注意,synchronized不能放在方法回傳類型的後面,如下面的程式碼是錯誤的:
複製代碼代碼如下:
public void synchronized method();
public static void synchronized method();
synchronized關鍵字只能用來同步方法,不能用來同步類別變量,如下面的程式碼也是錯誤的。
複製代碼代碼如下:
public synchronized int n = 0;
public static synchronized int n = 0;
雖然使用synchronized關鍵字同步方法是最安全的同步方式,但大量使用synchronized關鍵字會造成不必要的資源消耗以及效能損失。雖然從表面上看synchronized鎖定的是一個方法,但實際上synchronized鎖定的是一個類別。也就是說,如果在非靜態方法method1和method2定義時都使用了synchronized,在method1未執行完之前,method2是不能執行的。靜態方法和非靜態方法的情況類似。但靜態和非靜態方法不會互相影響。看看如下的程式碼:
複製代碼代碼如下:
package test;
public class MyThread1 extends Thread
{
public String methodName;
public static void method(String s)
{
System.out.println(s);
while (true)
}
public synchronized void method1()
{
method("非靜態的method1方法");
}
public synchronized void method2()
{
method("非靜態的method2方法");
}
public static synchronized void method3()
{
method("靜態的method3方法");
}
public static synchronized void method4()
{
method("靜態的method4方法");
}
public void run()
{
try
{
getClass().getMethod(methodName).invoke(this);
}
catch (Exception e)
{
}
}
public static void main(String[] args) throws Exception
{
MyThread1 myThread1 = new MyThread1();
for (int i = 1; i <= 4; i++)
{
myThread1.methodName = "method" + String.valueOf(i);
new Thread(myThread1).start();
sleep(100);
}
}
}
運行結果如下:
複製代碼代碼如下:
非靜態的method1方法
靜態的method3方法
從上面的運行結果可以看出,method2和method4在method1和method3未結束之前不能運行。因此,我們可以得出一個結論,如果在類別中使用synchronized關鍵字來定義非靜態方法,那將影響這個中的所有使用synchronized關鍵字定義的非靜態方法。如果定義的是靜態方法,那麼將影響類別中所有使用synchronized關鍵字定義的靜態方法。這有點象資料表中的表鎖,當修改一筆記錄時,系統就將整個表都鎖住了,因此,大量使用這種同步方式會使程式的效能大幅下降。