線程主要經過共享對字段和引用對象的引用字段的訪問來進行通訊,這種通訊形式很是有效,但可能產生兩種錯誤:線程干擾和內存一致性錯誤,防止這些錯誤所需的工具是同步。html
可是,同步可能會引入線程競爭,當兩個或多個線程同時嘗試訪問同一資源並致使Java運行時更慢地執行一個或多個線程,甚至暫停它們執行,飢餓和活鎖是線程競爭的形式。java
本節包括如下主題:c++
考慮一個名爲Counter的簡單類:git
class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c--; } public int value() { return c; } }
Counter
的設計爲每次increment
的調用都會將c
加1,每次decrement
的調用都會從c
中減去1,可是,若是從多個線程引用Counter
對象,則線程之間的干擾可能會妨礙這種狀況按預期發生。程序員
當兩個操做在不一樣的線程中運行但做用於相同的數據時,會發生干擾,這意味着這兩個操做由多個步驟組成,而且步驟序列交疊。github
對於Counter
實例的操做彷佛不可能進行交錯,由於對c
的兩個操做都是單個簡單的語句,可是,即便是簡單的語句也能夠由虛擬機轉換爲多個步驟,咱們不會檢查虛擬機採起的具體步驟 — 只需知道單個表達式c++
能夠分解爲三個步驟:編程
c
的當前值。表達式c--
能夠以相同的方式分解,除了第二步是遞減而不是遞增。segmentfault
假設在大約同一時間,線程A
調用increment
,線程B調用decrement
,若是c
的初始值爲0
,則它們的交錯操做可能遵循如下順序:api
c
。c
。1
。-1
。c
中,c
如今是1
。c
中,c
如今是-1
。線程A的結果丟失,被線程B覆蓋,這種特殊的交錯只是一種可能性,在不一樣的狀況下,多是線程B的結果丟失,或者根本沒有錯誤,由於它們是不可預測的,因此難以檢測和修復線程干擾錯誤。安全
當不一樣的線程具備應該是相同數據的不一致視圖時,會發生內存一致性錯誤,內存一致性錯誤的緣由很複雜,超出了本教程的範圍,幸運的是,程序員不須要詳細瞭解這些緣由,所須要的只是避免它們的策略。
避免內存一致性錯誤的關鍵是理解先發生關係,這種關係只是保證一個特定語句的內存寫入對另外一個特定語句可見,要了解這一點,請考慮如下示例,假設定義並初始化了一個簡單的int
字段:
int counter = 0;
counter
字段在兩個線程A和B之間共享,假設線程A遞增counter
:
counter++;
而後,不久以後,線程B打印出counter
:
System.out.println(counter);
若是兩個語句已在同一個線程中執行,則能夠安全地假設打印出的值爲「1
」,但若是兩個語句在不一樣的線程中執行,則打印出的值可能爲「0
」,由於沒法保證線程A對counter
的更改對線程B可見 — 除非程序員在這兩條語句之間創建了先發生關係。
有幾種操做能夠建立先發生關係,其中之一是同步,咱們將在下面的部分中看到。
咱們已經看到了兩種建立先發生關係的操做。
Thread.start
時,與該語句具備一個先發生關係的每一個語句也與新線程執行的每一個語句都有一個先發生關係,致使建立新線程的代碼的效果對新線程可見。Thread.join
返回時,已終止的線程執行的全部語句與成功join
後的全部語句都有一個先發生關係,線程中代碼的效果如今對執行join
的線程可見。有關建立先發生關係的操做列表,請參閱java.util.concurrent包的Summary頁面。
Java編程語言提供了兩種基本的同步語法:同步方法和同步語句,下兩節將介紹兩個同步語句中較爲複雜的語句,本節介紹同步方法。
要使方法同步,只需將synchronized
關鍵字添加到其聲明:
public class SynchronizedCounter { private int c = 0; public synchronized void increment() { c++; } public synchronized void decrement() { c--; } public synchronized int value() { return c; } }
若是count
是SynchronizedCounter
的一個實例,那麼使這些方法同步有兩個效果:
請注意,構造函數沒法同步 — 將synchronized
關鍵字與構造函數一塊兒使用是一種語法錯誤,同步構造函數沒有意義,由於只有建立對象的線程在構造時才能訪問它。
構造將在線程之間共享的對象時,要很是當心對對象的引用不會過早「泄漏」,例如,假設你要維護一個包含每一個類實例的名爲instances
的List
,你可能想要將如下行添加到你的構造函數中:instances.add(this);
可是其餘線程能夠在構造對象完成以前使用
instances
來訪問對象。
同步方法支持一種簡單的策略來防止線程干擾和內存一致性錯誤:若是一個對象對多個線程可見,則對該對象的變量全部讀取或寫入都是經過synchronized
方法完成的(一個重要的例外:一旦構造了對象,就能夠經過非同步方法安全地讀取構造對象後沒法修改的final
字段),這種策略頗有效,但可能會帶來活性問題,咱們將在本課後面看到。
同步是圍繞稱爲固有鎖或監控鎖的內部實體構建的(API規範一般將此實體簡稱爲「監視器」。),固有鎖在同步的兩個方面都起做用:強制執行對對象狀態的獨佔訪問,並創建對可見性相當重要的先發生關係。
每一個對象都有一個與之關聯的固有鎖,按照約定,須要對對象字段進行獨佔和一致訪問的線程必須在訪問對象以前獲取對象的固有鎖,而後在完成它們時釋放固有鎖。線程在獲取鎖和釋放鎖期間被稱爲擁有固有鎖,只要一個線程擁有固有鎖,沒有其餘線程能夠得到相同的鎖,另外一個線程在嘗試獲取鎖時將阻塞。
當線程釋放固有鎖時,在該操做與同一鎖的任何後續獲取之間創建先發生關係。
當線程調用同步方法時,它會自動獲取該方法對象的固有鎖,並在方法返回時釋放它,即便返回是由未捕獲的異常引發的,也會發生鎖定釋放。
你可能想知道調用靜態同步方法時會發生什麼,由於靜態方法與類相關聯,而不是與對象相關聯,在這種狀況下,線程獲取與類關聯的Class
對象的固有鎖,所以,對類的靜態字段的訪問由一個鎖控制,該鎖與該類的任何實例的鎖不一樣。
建立同步代碼的另外一種方法是使用同步語句,與同步方法不一樣,同步語句必須指定提供固有鎖的對象:
public void addName(String name) { synchronized(this) { lastName = name; nameCount++; } nameList.add(name); }
在此示例中,addName
方法須要同步更改lastName
和nameCount
,但還須要避免同步調用其餘對象的方法(從同步代碼中調用其餘對象的方法可能會產生有關活性一節中描述的問題),若是沒有同步語句,則必須有一個單獨的、不一樣步的方法,其惟一目的是調用nameList.add
。
同步語句對於經過細粒度同步提升併發性也頗有用,例如,假設類MsLunch
有兩個實例字段,c1
和c2
,它們從不一塊兒使用,必須同步這些字段的全部更新,可是沒有理由阻礙c1
的更新與c2
的更新交錯 — 而且這樣作會經過建立沒必要要的阻塞來減小併發性。咱們建立兩個對象只是爲了提供鎖,而不是使用同步方法或使用與此相關聯的鎖。
public class MsLunch { private long c1 = 0; private long c2 = 0; private Object lock1 = new Object(); private Object lock2 = new Object(); public void inc1() { synchronized(lock1) { c1++; } } public void inc2() { synchronized(lock2) { c2++; } } }
謹慎使用這種用法,你必須絕對確保對受影響字段的交錯訪問是安全的。
回想一下,線程沒法獲取另外一個線程擁有的鎖,可是一個線程能夠得到它已經擁有的鎖,容許線程屢次獲取同一個鎖可以使可重入同步。這描述了一種狀況,其中同步代碼直接或間接地調用也包含同步代碼的方法,而且兩組代碼使用相同的鎖,在沒有可重入同步的狀況下,同步代碼必須採起許多額外的預防措施,以免線程致使自身阻塞。
在編程中,原子操做是一次有效地同時發生的操做,原子操做不能停在中間:它要麼徹底發生,要麼根本不發生,在操做完成以前,原子操做的反作用在完成以前是不可見的。
咱們已經看到增量表達式(如c++
),沒有描述原子操做,即便很是簡單的表達式也能夠定義能夠分解爲其餘操做的複雜操做,可是,你能夠指定爲原子操做:
long
和double
以外的全部類型),讀取和寫入都是原子的。volatile
的全部變量(包括long
和double
),讀取和寫入都是原子的。原子操做不能交錯,所以可使用它們而不用擔憂線程干擾,可是,這並不能消除全部同步原子操做的須要,由於仍然可能存在內存一致性錯誤。使用volatile
變量能夠下降內存一致性錯誤的風險,由於對volatile
變量的任何寫入都會創建與以後讀取相同變量的先發生關係,這意味着對volatile
變量的更改始終對其餘線程可見。更重要的是,它還意味着當線程讀取volatile
變量時,它不只會看到volatile
的最新更改,還會看到致使更改的代碼的反作用。
使用簡單的原子變量訪問比經過同步代碼訪問這些變量更有效,但程序員須要更加當心以免內存一致性錯誤,額外的功夫是否值得取決於應用程序的大小和複雜性。
java.util.concurrent包中的某些類提供了不依賴於同步的原子方法,咱們將在高級併發對象一節中討論它們。