並發編程之線程安全性


並發編程之線程安全性


一、什么是線程安全性


並發編程中要編寫線程安全的代碼,則必須對可變的共享狀態的訪問操作進行管理。


對象的狀態就是存儲在實例或者靜態變量中的數據,同時其狀態也包含其關聯對象的字段,比如字典集合既包含自己的狀態,

也包含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,第一個線程判斷 expensiveObjectnull,第二個線程有可能判斷也為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.過多的同步代碼往往會導致活躍性問題和性能問題在使用鎖的時候,我們應該清楚我們的代碼功能及執行時間,

無論是計算密集型操作還是阻塞型操作,如果鎖定時間過長都會帶來活躍性或者性能問題。




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM