如今開始進入線程編程中最重要的話題---數據同步,它是線程編程的核心,也是難點,就算咱們理解了數據同步的基本原理,可是咱們也沒法保證可以寫出正確的同步代碼,但基本原理是必須掌握的。java
要想理解數據同步的基本原理,首先就要明白,爲何咱們要數據同步?編程
public class CharacterDisplayCanvas extends JComponent implements CharacterListener { protected FontMetrics fm; protected char[] tmpChar = new char[1]; protected int fontHeight; public CharacterDisplayCanvas() { setFont(new Font("Monospaced", Font.BOLD, 18)); fm = Toolkit.getDefaultToolkit().getFontMetrics(getFont()); fontHeight = fm.getHeight(); } public CharacterDisplayCanvas(CharacterSource cs) { this(); setCharacterSource(cs); } public void setCharacterSource(CharacterSource cs) { cs.addCharacterListener(this); } public synchronized void newCharacter(CharacterEvent ce) { tmpChar[0] = (char) ce.character; repaint(); } public Dimension preferredSize() { return new Dimension(fm.getMaxAscent() + 10, fm.getMaxAdvance() + 10); } protected synchronized void paintComponent(Graphics gc) { Dimension d = getSize(); gc.clearRect(0, 0, d.width, d.height); if (tmpChar[0] == 0) { return; } int charWidth = fm.charWidth((int) tmpChar[0]); gc.drawChars(tmpChar, 0, 1, (d.width - charWidth) / 2, fontHeight); } }
仔細查看上面的代碼,咱們就會發現,有兩個方法的前面多了一個新的關鍵字:synchronized。讓咱們看看這兩個方法爲何要添加這個關鍵字。
newCharacter()用於顯示新字母,而paintComponent()負責調整和重畫canvas。這兩個方法存在着race condition,也就是競爭,由於它們訪問的是同一份數據,最重要的是它們是由不一樣的線程所調用的,這就致使咱們沒法保證它們的調用是按照正確的順序來進行,可能在newCharacter()方法未被調用前paintComponent()方法就已經從新繪製canvas。canvas
之因此產生競爭,除了這兩個方法訪問的是同一份數據以外,還和它們是非automic有關。咱們在初中的時候都學過,原子曾經被認爲是最小單元,不可分的,哪怕如今已經證實這是不正確的,但原子不可分的概念在計算機這裏保留了下來。 一個程序若是被認爲是automic,那麼就表示它是沒法被中斷的,不會有中間狀態。使用synchronized,就能保證該方法沒法被中斷,那麼其餘線程就沒法在該方法沒有完成前調用它。數組
結合對象鎖的知識,咱們能夠簡單的講解一下synchronized的原理:一個線程若是想要調用另外一個線程的synchronized方法,並且該方法正在被其餘線程調用,那麼這個線程就必須等待,等待其餘線程釋放該方法所在的對象的鎖,而後得到該鎖執行該方法。鎖機制可以確保同一時間只有一個線程可以調用該方法,也就能保證只有一個線程可以訪問數據。多線程
還記得咱們以前經過使用標記來結束線程的時候,將該標記用volatile修飾?若是咱們不用volatile,又能使用什麼方法呢?優化
若是單單只是上面的知識,咱們可能會想到利用synchronized來同步run()和setDone(),由於就是這兩個方法在競爭done這個數據。可是這樣存在很大的問題:run()會在done沒有被設置true前永遠不會結束,可是done標記卻要等到run()方法結束後才能由setDone()方法進行設置。this
這就是一個死鎖,永遠解不開的鎖。spa
產生死鎖的緣由有不少,像是上面這種狀況就是一個典型的表明,主要緣由就是run()方法的scope(範圍)太大。所謂的scope,指的是獲取鎖到釋放鎖的時間,而run()方法的scope是一個循環,除非done設置爲true。這種須要依賴其餘線程的方法來結束執行的方法,若是將整個方法設置爲同步,就會出現死鎖。線程
因此,最好的方法就是將scope縮小。code
咱們能夠不用對整個方法進行同步,而是對須要訪問的數據進行同步,也就是對done使用volatile。
要想理解volatile的工做原理,咱們必須清楚變量的加載機制。java的內存模型容許線程可以在local memory中持有變量的值,因此這也就致使某個線程改變該變量的值時,其餘線程可能不會察覺到該變量的變化。這種狀況只是一種可能,並不表明必定會出現,但像是循環執行這種操做,就增長了這種可能。
因此,咱們要作的事情其實很簡單,就是讓線程從同一個地方取出變量而不是本身維護一份。使用volatile,每次使用該變量都要從主存儲器中讀取,每次改變該變量時,也要存入主存儲器,並且加載和存儲都是automic,不管是不是long或者double變量(這兩種類型的存儲是非automic的)。
值得注意的,run()方法和setDone()方法自己就是automic,由於setDone()方法僅有一個存儲操做,而run()方法也只有一個讀取操做,其他部分根本就須要該值保持不變,也就是說,這兩個方法其實自己就不存在競爭。
固然,若是仍是堅持想要使用synchronized的話,卻是有個比較醜陋的方法:對done提供setter和getter,而後synchronized這兩個方法,由於取得同步化的鎖表明全部暫時存儲於寄存器的值都會被清空到主存儲器中,這樣run()方法中要想取得done就必須等到setDone()方法設置完畢。
多麼醜陋的實現啊!!就爲了同步一個變量,結果咱們就要平白對兩個方法進行同步,增長無謂的線程開銷!!但這也是沒有辦法的事,若是咱們不知道還有volatile的話,沒準還會爲本身的小聰明而開心不已!!
這就是多線程編程的現實,若是咱們沒法知道還有更加優雅的實現,咱們永遠也只能寫出這樣的代碼。
但讓人更加困惑的是,volatile自己的存在如今也引發人們的關注:它到底有沒有必要?
volatile是以moot point(未決點)來實現的:變量永遠都從主存儲器中讀取,但這也只是JDK 1.2以前的狀況,如今的虛擬機實現使得內存模式愈來愈複雜,並且也獲得了極大的優化,而且這種趨勢只會一直持續下去。也就是說,基於內存模式的volatile可能會由於內存模式的不斷優化而逐漸變得沒有意義。
volatile的使用是有侷限的,它僅僅解決因內存模式而引起的問題,並且只能用在對變量的automic操做上,也就是訪問該變量的方法只能夠有單一的加載或者存儲。但不少方法都是非automic,像是遞增或者遞減操做,就容許存在中間狀態,由於它們自己就是載入,變動和存儲的簡化而已,也就是所謂的syntactic sugar(語法糖)。
咱們大概能夠這樣理解volatile的使用條件:強迫虛擬機不要臨時複製變量,哪怕咱們在許多狀況下都不會使用它們。
volatile是否能夠運用在數組上,讓整個數組中的全部元素都被同步呢?凡是使用java的人都會對這樣的幻想嗤之以鼻,由於實際狀況是隻有數組的引用纔會被同步,數組中的元素不會是volatile的,虛擬機仍是能夠將個別元素存儲於local的寄存器中,沒有任何方法能夠指定數組的元素應該以volatile的方式來處理。
咱們上面的同步問題是發生在展現隨機數字與字母的顯示組件,如今咱們繼續將功能完善:玩家能夠輸入所顯示的字母,而且正確就會得分。