線程安全是多線程領域的問題,線程安全可以簡單了解為一個方法或者一個執行個體可以在多線程環境中使用而不會出現問題。
在同一程式中運作多個線程本身不會導緻問題,問題在于多個線程通路了相同的資源。如,同一記憶體區(變量,數組,或對象)、系統(資料庫,web services等)或檔案。實際上,這些問題隻有在一或多個線程向這些資源做了寫操作時才有可能發生,隻要資源沒有發生變化,多個線程讀取相同的資源就是安全的。
多線程同時執行下面的代碼可能會出錯:
想象下線程a和b同時執行同一個counter對象的add()方法,我們無法知道作業系統何時會在兩個線程之間切換。jvm并不是将這段代碼視為單條指令來執行的,而是按照下面的順序:
觀察線程a和b交錯執行會發生什麼:
兩個線程分别加了2和3到count變量上,兩個線程執行結束後count變量的值應該等于5。然而由于兩個線程是交叉執行的,兩個線程從記憶體中讀出的初始值都是0。然後各自加了2和3,并分别寫回記憶體。最終的值并不是期望的5,而是最後寫回記憶體的那個線程的值,上面例子中最後寫回記憶體的是線程a,但實際中也可能是線程b。如果沒有采用合适的同步機制,線程間的交叉執行情況就無法預料。
當兩個線程競争同一資源時,如果對資源的通路順序敏感,就稱存在競态條件。導緻競态條件發生的代碼區稱作臨界區。上例中<code>add()</code>方法就是一個臨界區,它會産生競态條件。在臨界區中使用适當的同步就可以避免競态條件。
允許被多個線程同時執行的代碼稱作線程安全的代碼。線程安全的代碼不包含競态條件。當多個線程同時更新共享資源時會引發競态條件。是以,了解java線程執行時共享了什麼資源很重要。
局部變量存儲線上程自己的棧中。也就是說,局部變量永遠也不會被多個線程共享。是以,基礎類型的局部變量是線程安全的。下面是基礎類型的局部變量的一個例子:
上面提到的局部變量是一個基本類型,如果局部變量是一個對象類型呢?對象的局部引用和基礎類型的局部變量不太一樣。盡管引用本身沒有被共享,但引用所指的對象并沒有存儲線上程的棧内,所有的對象都存在共享堆中,是以對于局部對象的引用,有可能是線程安全的,也有可能是線程不安全的。
那麼怎樣才是線程安全的呢?如果在某個方法中建立的對象不會被其他方法或全局變量獲得,或者說方法中建立的對象沒有逃出此方法的範圍,那麼它就是線程安全的。實際上,哪怕将這個對象作為參數傳給其它方法,隻要别的線程擷取不到這個對象,那它仍是線程安全的。下面是一個線程安全的局部引用樣例:
上面樣例中<code>localobject</code>對象沒有被方法傳回,也沒有被傳遞給<code>somemethod()</code>方法外的對象,始終在<code>somemethod()</code>方法内部。每個執行<code>somemethod()</code>的線程都會建立自己的<code>localobject</code>對象,并指派給localobject引用。是以,這裡的<code>localobject</code>是線程安全的。事實上,整個<code>somemethod()</code>都是線程安全的。即使将<code>localobject</code>作為參數傳給同一個類的其它方法或其它類的方法時,它仍然是線程安全的。當然,如果<code>localobject</code>通過某些方法被傳給了别的線程,那它就不再是線程安全的了。
對象成員對象成員存儲在堆上。如果兩個線程同時更新同一個對象的同一個成員,那這個代碼就不是線程安全的。下面是一個樣例:
如果兩個線程同時調用同一個<code>notthreadsafe</code>執行個體上的<code>add()</code>方法,就會有競态條件問題。例如:
注意兩個myrunnable共享了同一個notthreadsafe對象。是以,當它們調用<code>add()</code>方法時會造成競态條件。
當然,如果這兩個線程在不同的notthreadsafe執行個體上調用call()方法,就不會導緻競态條件。下面是稍微修改後的例子:
現在兩個線程都有自己單獨的notthreadsafe對象,通路的不是同一資源,不滿足競态條件,是線程安全的。是以非線程安全的對象仍可以通過某種方式來消除競态條件。
線程控制逃逸規則可以幫助你判斷代碼中對某些資源的通路是否是線程安全的。
資源可以是對象,數組,檔案,資料庫連接配接,套接字等等。java中我們無需主動銷毀對象,是以“銷毀”指不再有引用指向對象。
注意即使對象本身線程安全,但如果該對象中包含其他資源(檔案,資料庫連接配接),整個應用也許就不再是線程安全的了。比如2個線程都建立了各自的資料庫連接配接,每個連接配接自身是線程安全的,但它們所連接配接到的同一個資料庫也許不是線程安全的。比如,2個線程執行如下代碼:
如果兩個線程同時執行,而且碰巧檢查的是同一個記錄,那麼兩個線程最終可能都插入了記錄:
同樣的問題也會發生在檔案或其他共享資源上。是以,區分某個線程控制的對象是資源本身,還是僅僅到某個資源的引用很重要。
不可變的共享資源
當多個 線程同時通路同一個資源,并且其中的一個或者多個線程對這個資源進行了寫操作,才會産生競态條件。多個線程同時讀同一個資源不會産生競态條件。
我們可以通過建立不可變的共享對象來保證對象線上程間共享時不會被修改,進而實作線程安全。如下示例:
如果你需要對immutablevalue類的執行個體進行操作,如添加一個類似于加法的操作,我們不能對這個執行個體直接進行操作,隻能建立一個新的執行個體來實作,下面是一個對value變量進行加法操作的示例:
請注意<code>add()</code>方法以加法操作的結果作為一個新的immutablevalue類執行個體傳回,而不是直接對它自己的value變量進行操作。
重要的是要記住,即使一個對象是線程安全的不可變對象,指向這個對象的引用也可能不是線程安全的。看這個例子:
calculator類持有一個指向immutablevalue執行個體的引用。注意,通過<code>setvalue()</code>方法和<code>add()</code>方法可能會改變這個引用,是以,即使calculator類内部使用了一個不可變對象,但calculator類本身還是可變的,多個線程通路calculator執行個體時仍可通過<code>setvalue()</code>和add()方法改變它的狀态,是以calculator類不是線程安全的。
換句話說:immutablevalue類是線程安全的,但使用它的類則不一定是。當嘗試通過不可變性去獲得線程安全時,這點是需要牢記的。
要使calculator類實作線程安全,将<code>getvalue()</code>、<code>setvalue()</code>和<code>add()</code>方法都聲明為同步方法即可。
在java多線程程式設計當中,提供了多種實作java線程安全的方式:
使用<code>java.util.concurrent.atomic</code> 包中的原子類,例如 <code>atomicinteger</code>
使用<code>java.util.concurrent.locks</code> 包中的鎖
使用線程安全的集合<code>concurrenthashmap</code>
使用<code>volatile</code>關鍵字,保證變量可見性(直接從記憶體讀,而不是從線程cache讀)