線程間的通信主要是通過共享域和引用相同的對象。這種通信方式非常高效,不過可能會引發兩種錯誤:線程幹擾和記憶體一緻性錯誤。防止這些錯誤發生的方法是同步。
不過,同步會引起線程競争,當兩個或多個線程試圖同時通路相同的資源,随之就導緻java運作時環境執行其中一個或多個線程比原先慢很多,甚至執行被挂起,這就出現了線程競争。線程饑餓和活鎖都屬于線程競争的範疇。關于線程競争的更多資訊可參考活躍度一節。
本節内容包括以下這些主題:
線程幹擾讨論了當多個線程通路共享資料時錯誤是怎麼發生的。
記憶體一緻性錯誤讨論了不一緻的共享記憶體視圖導緻的錯誤。
同步方法讨論了 一種能有效防止線程幹擾和記憶體一緻性錯誤的常見做法。
内部鎖和同步讨論了更通用的同步方法,以及同步是如何基于内部鎖實作的。
原子通路讨論了不能被其他線程幹擾的操作的總體思路。
<a href="http://docs.oracle.com/javase/tutorial/essential/concurrency/interfere.html" target="_blank">原文連結</a>
下面這個簡單的counter類:
counter類被設計成:每次調用increment()方法,c的值加1;每次調用decrement()方法,c的值減1。如果當同一個counter對象被多個線程引用,線程間的幹擾可能會使結果同我們預期的不一緻。
當兩個運作在不同的線程中卻作用在相同的資料上的操作交替執行時,就發生了線程幹擾。這意味着這兩個操作都由多個步驟組成,而步驟間的順序産生了重疊。
counter類執行個體的操作會交替執行,這看起來似乎不太可能,因為c上的這兩個操作都是單一而簡單的語句。然而,即使一個簡單的語句也會被虛拟機轉換成多個步驟。我們不去深究虛拟機内部的詳細執行步驟——了解c++這個單一的語句會被分解成3個步驟就足夠了:
擷取目前c的值;
對擷取到的值加1;
把遞增後的值寫回到c;
語句c–也可以按同樣的方式分解,除了第二步的操作是遞減而不是遞增。
假設線程a調用increment()的同時線程b調用decrement().如果c的初始值為0,線程a和b之間的交替執行順序可能是下面這樣:
線程a:擷取c;
線程b:擷取c;
線程a:對擷取的值加1,結果為1;
線程b:對擷取的值減1,結果為-1;
線程a:結果寫回到c,c現在是1;
線程b:結果寫回到c,c現在是-1;
線程a的結果因為被線程b覆寫而丢失了。這個交替執行的結果隻是其中一種可能性。在不同的環境下,可能是線程b的結果丢失了,也可能是不會出任何問題。由于結果是不可預知的,是以線程幹擾的bug很難檢測和修複。
<a href="http://docs.oracle.com/javase/tutorial/essential/concurrency/memconsist.html" target="_blank">原文連結</a>
當不同的線程對相同的資料産生不一緻的視圖時會發生記憶體一緻性錯誤。記憶體一緻性錯誤的原因比較複雜,也超出了本教程的範圍。不過幸運的是,一個程式員并不需要對這些原因有詳細的了解。所需要的是避免它們的政策。

避免記憶體一緻性錯誤的關鍵是了解happens-before關系。這種關系隻是確定一個特定語句的寫記憶體操作對另外一個特定的語句可見。要說明這個問題,請參考下面的例子。假設定義和初始化了一個簡單int字段:
這個counter字段被a,b兩個線程共享。假設線程a對counter執行遞增:
然後,很快的,線程b輸出counter:
如果這兩個語句已經在同一個線程中被執行過,那麼輸出的值應該是“1”。不過如果這兩個語句在不同的線程中分開執行,那輸出的值很可能是“0”,因為無法保證線程a對counter的改動對線程b是可見的——除非我們在這兩個語句之間已經建立了happens-before關系。
有許多操作會建立happens-before關系。其中一個是同步,我們将在下面的章節中看到。
我們已經見過兩個建立happens-before關系的操作。
當一條語句調用thread.start方法時,和該語句有happens-before關系的每一條語句,跟新線程執行的每一條語句同樣有happens-before關系。建立新線程之前的代碼的執行結果對線新線程是可見的。
當一個線程終止并且當導緻另一個線程中thread.join傳回時,被終止的線程執行的所有語句和在join傳回成功之後的所有語句間有happens-before關系。線程中代碼的執行結果對執行join操作的線程是可見的。
<a href="http://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html">原文位址</a>
java程式設計語言提供兩種同步方式:同步方法和同步語句。相對較複雜的同步語句将在下一節中介紹。本節主要關注同步方法。
要讓一個方法成為同步方法,隻需要在方法聲明中加上synchronized關鍵字:

如果count是synchronizedcounter類的執行個體,那麼讓這些方法成為同步方法有兩個作用:
首先,相同對象上的同步方法的兩次調用,它們要交替執行是不可能的。 當一個線程正在執行對象的同步方法時,所有其他調用該對象同步方法的線程會被阻塞(挂起執行),直到第一個線程處理完該對象。
其次,當一個同步方法退出時,它會自動跟該對象同步方法的任意後續調用建立起一種happens-before關系。這確定對象狀态的改變對所有線程是可見的。
注意構造方法不能是同步的——構造方法加synchronized關鍵字會報文法錯誤。同步的構造方法沒有意義,因為當這個對象被建立的時候,隻有建立對象的線程能通路它。
警告:當建立的對象會被多個線程共享時必須非常小心,對象的引用不要過早“暴露”出去。比如,假設你要維護一個叫instances的list,它包含類的每一個執行個體對象。你可能會嘗試在構造方法中加這樣一行:
不過其他線程就能夠在對象構造完成之前使用instances通路對象。
同步(synchronized)方法使用一種簡單的政策來防止線程幹擾和記憶體一緻性錯誤:如果一個對象對多個線程可見,對象域上的所有讀寫操作都是通過synchronized方法來完成的。(一個重要的例外:final域,在對象被建立後不可修改,能被非synchronized方法安全的讀取)。synchronized同步政策很有效,不過會引起活躍度問題,我們将在本節後面看到。
<a href="http://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html">原文連結</a>
同步機制的建立是基于其内部一個叫内部鎖或者監視鎖的實體。(在java api規範中通常被稱為螢幕。)内部鎖在同步機制中起到兩方面的作用:對一個對象的排他性通路;建立一種happens-before關系,而這種關系正是可見性問題的關鍵所在。
每個對象都有一個與之關聯的内部鎖。通常當一個線程需要排他性的通路一個對象的域時,首先需要請求該對象的内部鎖,當通路結束時釋放内部鎖。線上程獲得内部鎖到釋放内部鎖的這段時間裡,我們說線程擁有這個内部鎖。那麼當一個線程擁有一個内部鎖時,其他線程将無法獲得該内部鎖。其他線程如果去嘗試獲得該内部鎖,則會被阻塞。
當線程釋放一個内部鎖時,該操作和對該鎖的後續請求間将建立happens-before關系。

當線程調用一個同步方法時,它會自動請求該方法所在對象的内部鎖。當方法傳回結束時則自動釋放該内部鎖,即使退出是由于發生了未捕獲的異常,内部鎖也會被釋放。
你可能會問調用一個靜态的同步方法會如何,由于靜态方法是和類(而不是對象)相關的,是以線程會請求類對象(class object)的内部鎖。是以用來控制類的靜态域通路的鎖不同于控制對象通路的鎖。
另外一種同步的方法是使用同步塊。和同步方法不同,同步塊必須指定所請求的是哪個對象的内部鎖:
使用同步塊對于更細粒度的同步很有幫助。例如類mslunch有兩個執行個體域c1和c2,他們并不會同時使用(譯者注:即c1和c2是彼此無關的兩個域),所有對這兩個域的更新都需要同步,但是完全不需要防止c1的修改和c2的修改互相之間幹擾(這樣做隻會産生不必要的阻塞而降低了并發性)。這種情況下不必使用同步方法,可以使用和this對象相關的鎖。這裡我們建立了兩個“鎖”對象(譯者注:起到加鎖效果的普通對象lock1和lock2)。
使用這種方法時要特别小心,需要十分确定c1和c2是彼此無關的域。
還記得嗎,一個線程不能獲得其他線程所擁有的鎖。但是它可以獲得自己已經擁有的鎖。允許一個線程多次獲得同一個鎖實作了可重入同步。這裡描述了一種同步代碼的場景,直接的或間接地,調用了一個也擁有同步代碼的方法,且兩邊的代碼使用的是同一把鎖。如果沒有這種可重入的同步機制,同步代碼則需要采取許多額外的預防措施以防止線程阻塞自己。
<a href="http://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html">原文連結</a>
在程式設計過程中,原子操作是指所有操作都同時發生。原子操作不能被中途打斷:要麼全做,要麼不做。原子操作在完成前不會有看得見的副作用。
我們發現像<code>c++</code>這樣的增量表達式,并沒有描述原子操作。即使是非常簡單的表達式也能夠定義成能被分解為其他操作的複雜操作。然而,有些操作你可以定義為原子的:
對引用變量和大部分基本類型變量(除long和double之外)的讀寫是原子的。
對所有聲明為volatile的變量(包括long和double變量)的讀寫是原子的。
原子操作不會交錯,于是可以放心使用,不必擔心線程幹擾。然而,這并不能完全消除原子操作上的同步,因為記憶體一緻性錯誤仍可能發生。使用volatile變量可以降低記憶體一緻性錯誤的風險,因為對volatile變量的任意寫操作,對于後續在該變量上的讀操作建立了happens-before關系。這意味着volatile變量的修改對于其他線程總是可見的。更重要的是,這同時也意味着當一個線程讀取一個volatile變量時,它不僅能看到該變量最新的修改,而且也能看到緻使該改變發生的代碼的副效應。
使用簡單的原子變量通路比通過同步代碼來通路更高效,但是需要程式員更加謹慎以避免記憶體一緻性錯誤。至于這額外的付出是否值得,得看應用的大小和複雜度。
<code>java.util.concurrent</code>包中的一些類提供了一些不依賴同步機制的原子方法。我們将在進階并發對象這一節中讨論它們。