同步有兩種屬性:互斥性和可見性。synchronized關鍵字與兩者都有關系。Java同時也提供了一種更弱的、僅僅包含可見性的同步形式,并且只以volatile關鍵字關聯。
假設你自己設計了一個停止線程的機制(因為無法使用Thread不安全的stop()方法))。清單1中ThreadStopping程序源碼展示了該如何完成這項任務。
清單1 嘗試停止一個線程
public class ThreadStopping{ public static void main(String[] args) { class StoppableThread extends Thread { private boolean stopped; // defaults to false @Override public void run() { while(!stopped) System.out.println("running"); } void stopThread() { stopped = true; } } StoppableThread thd = new StoppableThread(); thd.start(); try { Thread.sleep(1000); // sleep for 1 second } catch (InterruptedException ie) { } thd.stopThread(); } }
清單2中的main()方法聲明了一個叫做StoppableThread的本地類,它繼承自Thread。在初始化完StoppableThread之后,默認的主線程啟動和這個 Thread對象關聯的線程。之后它睡眠 1 秒,并且在死亡之前調用StoppableThread的stop()方法。
StoppableThread聲明了一個被初始化為false的stopped實例變量,stopThread()方法會將該變量設置為true,同時run()方法中的while循環會在每次迭代中檢查stopped的值是否已經修改為true。
照下面編譯清單2:
javac ThreadStopping.java
運行程序:
java ThreadStopping 你應該能觀測到一系列運行時的消息。
當你在單處理器/單核的機器上運行這個程序的時候,很可能會觀測到程序停止。但是在一個多處理器的機器或多核單處理器的機器上,可能就看不到程序停止,因為每個處理器或者核心很可能有自己的一份stopped的拷貝,當一條線程修改了自己的拷貝,其他線程的拷貝并沒有被改變。
你或許決定使用synchronized關鍵字以確保只能訪問主存中的stopped變量。然后經過一番思考,你決定在清單3中使用同步訪問一對臨界區的方式來解決這個問題。
清單3 嘗試使用synchronized來停止一個線程
public class ThreadStopping{ public static void main(String[] args) { class StoppableThread extends Thread { private boolean stopped; // defaults to false @Override public void run() { synchronized(this) { while(!stopped) System.out.println("running"); } } synchronized void stopThread() { stopped = true; } } StoppableThread thd = new StoppableThread(); thd.start(); try { Thread.sleep(1000); // sleep for 1 second } catch (InterruptedException ie) { } thd.stopThread(); }}
出于兩個因素考慮,清單3不是一個好主意。盡管你只需解決可見性的問題,synchronized卻同時解決了互斥的問題(在該程序中不是個問題)。更重要的是,你還往程序中引進了另一個更嚴重的問題。
你已經正確地對stopped進行了同步訪問,但是進一步觀察run()方法中的同步塊,尤其是這個while循環。由于正在執行循環的這個線程已經獲取了當前StoppableThread對象(通過synchronized(this))的鎖,這個循環不會終止。因為默認的主線程需要獲取相同的鎖,所以它在該對象上調用stopThread()方法的任意嘗試都會導致自己被阻塞住。
你可以使用局部變量并在同步塊中將stopped的值賦給這個變量來解決這一問題,如下所示:
public void run(){ boolean _stopped = false; while (!_stopped) { synchronized(this) { _stopped = stopped; } System.out.println("running"); }}
不過,每次循環迭代都要嘗試獲取鎖的方式會存在性能開銷(還不如以前),所以這個解決方式是得不償失的。清單4展示了一個更為高效且整潔的方法。
清單4 嘗試通過volatile關鍵字來停止一個線程
public class ThreadStopping{ public static void main(String[] args) { class StoppableThread extends Thread { private #####volatile boolean stopped; // defaults to false @Override public void run() { while(!stopped) System.out.println("running"); } void stopThread() { stopped = true; } } StoppableThread thd = new StoppableThread(); thd.start(); try { Thread.sleep(1000); // sleep for 1 second } catch (InterruptedException ie) { } thd.stopThread(); } }
由于stopped已經標記為volatile,每條線程都會訪問主存中該變量的拷貝而不會訪問緩存中的拷貝。這樣,即使在多處理器或者多核的機器上,該程序也會停止。
警告
只有可見性導致問題時,才應該使用volatile。而且,你也只能在屬性聲明處才能使用這個保留字(如果你嘗試將局部變量聲明成volatitle,會收到一個錯誤)。最后,你可以將double和long型的屬性聲明成volatile,但是應該避免在32位的JVM上這樣做,原因是此時訪問一個double或者long型的變量值需要進行兩步操作,若要安全地訪問它們的值,互斥(通過synchronized)是必要的。 當一個屬性變量聲明成volatile,就不能同時被聲明final的。不過,由于Java可以讓你安全地訪問final的屬性而無需同步,這也就不能稱之為一個問題了。為了克服DeadlockDemo中的緩存變量問題,我把lock1和lock2都標記成final,盡管也能將它們標記成volatile的。
以后,你會經常使用final關鍵字來確保在不可變(不會發生改變)類的上下文中線程的安全性。參考清單5。
清單5 借助于final創建一個不可變且線程安全的類
import java.util.Set;import java.util.TreeSet;public final class Planets{ private final Set planets = new TreeSet<>(); public Planets() { planets.add("Mercury"); planets.add("Venus"); planets.add("Earth"); planets.add("Mars"); planets.add("Jupiter"); planets.add("Saturn"); planets.add("Uranus"); planets.add("Neptune"); } public boolean isPlanet(String planetName) { return planets.contains(planetName); }}
清單5展示了一個不可變類Planets,其對象存儲著星球名字的集合。盡管集合是可變的,但這個類的設計卻保證在構造函數退出之后,集合不會再被改變。通過聲明planets為final,這個屬性的引用不能被更改。而且,該引用也不能被緩存,所以緩存變量的問題也不復存在。
關于不可變對象,Java提供了一種特殊的線程安全的保證。即便沒有用同步來發布(暴露)這些對象的引用,它們依然可以被多條線程安全地訪問。不可變對象提供了下列易于識別的規則:
- 不可變對象絕對不允許狀態變更。
- 所有的屬性必須聲明成final。
- 對象必須被恰當地構造出來以防this引用脫離構造函數。
最后一點很讓人迷惑,所以這里給出一個this顯式地脫離構造函數的簡單例子:
public class ThisEscapeDemo{ private static ThisEscapeDemo lastCreatedInstance; public ThisEscapeDemo() { lastCreatedInstance = this; }}
在www.ibm.com/developerworks/library/j-jtp0618/上查看《Java theory and practice:Safe construction techniques》學習更多常見線程風險的相關知識。
本文節選自《Java線程與并發編程實踐》

Java線程和并發工具是應用開發中的重要部分,備受開發者的重視,也有一定的學習難度。
本書是針對Java 8中的線程特性和并發工具的快速學習和實踐指南。全書共8章,分別介紹了Thread類和Runnable接口、同步、等待和通知、線程組、定時器框架、并發工具、同步器、鎖框架,以及高級并發工具等方面的主題。每章的末尾都以練習題的方式,幫助讀者鞏固所學的知識。附錄A給出了所有練習題的解答,附錄B給出了一個基于Swing線程的教程。
本書適合有一定基礎的Java程序員閱讀學習,尤其適合想要掌握Java線程和并發工具的讀者閱讀參考。