併發編程之線程安全性java
1、什麼是線程安全性編程
併發編程中要編寫線程安全的代碼,則必須對可變的共享狀態的訪問操做進行管理。安全
對象的狀態就是存儲在實例或者靜態變量中的數據,同時其狀態也包含其關聯對象的字段,好比字典集合既包含本身的狀態,多線程
也包含KeyValuePair。併發
共享便可以多個線程同時訪問變量,可變即變量在其聲明週期內能夠發生變化。app
代碼線程安全性關注的是防止對數據進行不可控的併發訪問。性能
是否以多線程的方式訪問對象,決定了此對象是否須要線程安全性。線程安全性強調的是對對象的訪問方式,而不是對象this
要實現的功能。要實現線程安全性,則須要採用同步機制來協調對對象可變狀態的訪問。例如當修改一個可能會有多個線atom
程同時訪問的狀態變量的時候,必須採用同步機制協調這些線程對變量的訪問,不然可能致使數據被破壞或者致使不可預spa
知的結果。
保證線程安全性的三種方式
不共享狀態變量
共享不可變狀態變量
同步對狀態變量的訪問和操做
面向對象的封裝特性有利於咱們編寫結構優雅、可維護性高的線程安全代碼。
當多個線程訪問某個類時,其始終都能表現出正確的行爲,那這個類就是線程安全的。類的正確性是由類的規範定義的,
其規範包含約束對象狀態的不變性條件和描述對象操做結果的後驗條件。例如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); } }
2、原子性
若是咱們在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); } }
3、鎖定機制
若是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(); } }
4、加鎖同步須要注意的問題
1.訪問共享狀態的符合操做,須要在訪問狀態變量的全部位置都須要使用同步,
而且每一個位置都須要使用同一個鎖。
2.對象內置鎖並不阻止其餘線程對此對象的訪問,只能阻止其獲取同一個鎖,須要咱們本身實現同步策略確保對共享狀態
的安全訪問。
3.將全部的可變狀態都封裝在對象內部,並經過對象內置鎖對全部訪問狀態的代碼進行同步,是一種常見的加鎖策略。
可是有時並不能保證複合操做的原子性。
if(!array.contains(element)) { //比較耗費時間的業務操做 array.add(element); }
4.過多的同步代碼每每會致使活躍性問題和性能問題。在使用鎖的時候,咱們應該清楚咱們的代碼功能及執行時間,
不管是計算密集型操做仍是阻塞型操做,若是鎖定時間過長都會帶來活躍性或者性能問題。