併發程式設計之執行緒安全性
一、什麼是執行緒安全性
併發程式設計中要編寫執行緒安全的程式碼,則必須對可變的共享狀態的訪問操作進行管理。
物件的狀態就是儲存在例項或者靜態變數中的資料,同時其狀態也包含其關聯物件的欄位,比如字典集合既包含自己的狀態,
也包含KeyValuePair。
共享即可以多個執行緒同時訪問變數,可變即變數在其宣告週期內可以發生變化。
程式碼執行緒安全性關注的是防止對資料進行不可控的併發訪問。
是否以多執行緒的方式訪問物件,決定了此物件是否需要執行緒安全性。執行緒安全性強調的是對物件的訪問方式,而不是物件
要實現的功能。要實現執行緒安全性,則需要採用同步機制來協調對物件可變狀態的訪問。例如當修改一個可能會有多個線
程同時訪問的狀態變數的時候,必須採用同步機制協調這些執行緒對變數的訪問,否則可能導致資料被破壞或者導致不可預
知的結果。
保證執行緒安全性的三種方式
不共享狀態變數
共享不可變狀態變數
同步對狀態變數的訪問和操作
物件導向的封裝特性有利於我們編寫結構優雅、可維護性高的執行緒安全程式碼。
當多個執行緒訪問某個類時,其始終都能表現出正確的行為,那這個類就是執行緒安全的。類的正確性是由類的規範定義的,
其規範包含約束物件狀態的不變性條件和描述物件操作結果的後驗條件。例如Servlet規範規定Servlet在站點啟動時或者
第一次請求訪問時進行初始化,後續再次請求則不會進行初始化,因為Servelet會被多個執行緒訪問,所以為了保證其執行緒
安全性,其只能是無狀態的或者對狀態訪問進行同步。
package com.codeartist; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class HelloConcurrentWorldServlet */ @WebServlet("/HelloConcurrentWorldServlet") public class HelloConcurrentWorldServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public HelloConcurrentWorldServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub response.getWriter().append("Hello Concurrent World ! from codeartist! "); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
二、原子性
如果我們在Servlet中新增一個統計訪問次數的狀態欄位,會出現什麼情況呢?
package com.codeartist; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class CountorServlet */ @WebServlet("/CountorServlet") public class CountorServlet extends HttpServlet { private static final long serialVersionUID = 1L; private long acessCount=0; /** * @see HttpServlet#HttpServlet() */ public CountorServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub acessCount++; response.getWriter().append("Welcome your acess my Servelet ! ,you are " + acessCount+ " visitor."); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
我們知道Servlet並不是執行緒安全的,其中acessCount++只是看起來像一個操作的緊湊語法,其本身並不是一個不可分割的
原子性操作。實際上其包含三個獨立的操作:讀取acessCount的值,將其值遞增1,然後將計算結果存入acessCount。這是一個依
賴操作順序的操作序列。如果兩個請求同時讀取acessCount的值,最終會導致丟失一次訪問記錄。
在併發程式設計中,這種由於執行時序導致不確定結果的情況,有一個更專業的稱謂“竟態條件”。開發中常見的竟態條件就
是“先檢查後執行操作”,即基於可能失效的檢測條件決定下一步的操作,其中又以物件的延遲初始化比較多見
package com.codeartist; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class DelayInitExpensiveServlet */ @WebServlet("/DelayInitExpensiveServlet") public class DelayInitExpensiveServlet extends HttpServlet { private static final long serialVersionUID = 1L; private ExpensiveObject expensiveObject = null; public ExpensiveObject getExpensiveObject() { if(this.expensiveObject == null) { this.expensiveObject = new ExpensiveObject(); } return this.expensiveObject; } /** * @see HttpServlet#HttpServlet() */ public DelayInitExpensiveServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub response.getWriter().append("Served at: ").append(request.getContextPath()); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
如果有兩個執行緒同時執行 getExpensiveObject,第一個執行緒判斷 expensiveObject為null,第二個執行緒有可能判斷也為null
或者已經初始化完成,這除了依賴執行緒的執行次序,同時也依賴與初始化ExpensiveObject需要的事件長短。
在上邊的兩個例子中,我們必須在某個執行緒操作狀態變數的時候,通過某種方式限制其他執行緒只能在操作之前或者
完成之後操作狀態變數。其實就是要求這些符合操作要具有原子性,比如acessCount++,我們可以將其委託給執行緒
安全的AtomicLong來管理,從而確保了程式碼的執行緒安全性。
package com.codeartist; import java.io.IOException; import java.util.concurrent.atomic.AtomicLong; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class AtomicLongCountorServlet */ @WebServlet("/AtomicLongCountorServlet") publicclass AtomicLongCountorServlet extends HttpServlet { privatestaticfinallongserialVersionUID = 1L; private AtomicLong acessCount = new AtomicLong(0); /** * @see HttpServlet#HttpServlet() */ public AtomicLongCountorServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protectedvoid doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub this.acessCount.incrementAndGet(); response.getWriter().append("Welcome your acess my Servelet ! ,you are " + this.acessCount.get()+ " visitor."); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protectedvoid doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
三、鎖定機制
如果Sevlet中有多個相互關聯的狀態變數需要確保操作的時序怎麼辦呢?比如下邊簡單示意的轉賬程式碼。
package com.codeartist; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class TransformCash */ @WebServlet("/TransformCash") public class TransformCash extends HttpServlet { private static final long serialVersionUID = 1L; private CashAcount fromCashAcount ; private CashAcount toCashAcount ; /** * @see HttpServlet#HttpServlet() */ public TransformCash() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub // float cash =100; this.fromCashAcount.reduce(cash); this.toCashAcount.plus(cash); //response.getWriter().append("Served at: ").append(request.getContextPath()); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
沒錯就是通過加鎖對操作進行同步。java提供了Synchronized關鍵字來實現鎖定機制,執行緒在進入同步程式碼塊之前
會自動獲得鎖,並在推出程式碼的時候釋放鎖。此互斥鎖只能同時由一個執行緒持有,其他執行緒只能等待或者阻塞,
因此可以確保複合操作的原子性。
package com.codeartist; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class TransformCash */ @WebServlet("/TransformCash") public class TransformCash extends HttpServlet { private static final long serialVersionUID = 1L; private CashAcount fromCashAcount ; private CashAcount toCashAcount ; /** * @see HttpServlet#HttpServlet() */ public TransformCash() { super(); // TODO Auto-generated constructor stub } protected synchronized void transform() { float cash =100; this.fromCashAcount.reduce(cash); this.toCashAcount.plus(cash); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub // transform(); //response.getWriter().append("Served at: ").append(request.getContextPath()); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
java內建鎖除了互斥特性,為了避免死鎖的發生,它還具有重入特性,即某個執行緒可以重複申請獲取自己已經持有的鎖。
重入意味者鎖定操作的粒度是執行緒而不是呼叫,即會同時記錄申請的執行緒和次數。例如下邊在子類中重寫並呼叫父類
的synchronized 方法。
package com.codeartist; publicclass synchronizedParent { publicsynchronizedvoid initSomething() { } } package com.codeartist; publicclass synchronizedChild extends synchronizedParent { publicsynchronizedvoid initSomething() { super.initSomething(); } }
四、加鎖同步需要注意的問題
1.訪問共享狀態的符合操作,需要在訪問狀態變數的所有位置都需要使用同步,
並且每個位置都需要使用同一個鎖。
2.物件內建鎖並不阻止其他執行緒對此物件的訪問,只能阻止其獲取同一個鎖,需要我們自己實現同步策略確保對共享狀態
的安全訪問。
3.將所有的可變狀態都封裝在物件內部,並通過物件內建鎖對所有訪問狀態的程式碼進行同步,是一種常見的加鎖策略。
但是有時並不能保證複合操作的原子性。
if(!array.contains(element)) { //比較耗費時間的業務操作 array.add(element); }
4.過多的同步程式碼往往會導致活躍性問題和效能問題。在使用鎖的時候,我們應該清楚我們的程式碼功能及執行時間,
無論是計算密集型操作還是阻塞型操作,如果鎖定時間過長都會帶來活躍性或者效能問題。