本文目錄一覽:
如何保證線程安全?
1、不可變
在java語言中,不可變的對象一定是線程安全的,無論是對象的方法實現還是方法的調用者,都不需要再採取任何的線程安全保障措施。如final關鍵字修飾的數據不可修改,可靠性最高。
2、絕對線程安全
絕對的線程安全完全滿足Brian GoetZ給出的線程安全的定義,這個定義其實是很嚴格的,一個類要達到「不管運行時環境如何,調用者都不需要任何額外的同步措施」通常需要付出很大的代價。
3、相對線程安全
相對線程安全就是我們通常意義上所講的一個類是「線程安全」的。
它需要保證對這個對象單獨的操作是線程安全的,我們在調用的時候不需要做額外的保障措施,但是對於一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。
在java語言中,大部分的線程安全類都屬於相對線程安全的,例如Vector、HashTable、Collections的synchronizedCollection()方法保證的集合。
4、線程兼容
線程兼容就是我們通常意義上所講的一個類不是線程安全的。
線程兼容是指對象本身並不是線程安全的,但是可以通過在調用端正確地使用同步手段來保證對象在並發環境下可以安全地使用。Java API中大部分的類都是屬於線程兼容的。如與前面的Vector和HashTable相對應的集合類ArrayList和HashMap等。
5、線程對立
線程對立是指無論調用端是否採取了同步錯誤,都無法在多線程環境中並發使用的代碼。由於java語言天生就具有多線程特性,線程對立這種排斥多線程的代碼是很少出現的。
一個線程對立的例子是Thread類的supend()和resume()方法。如果有兩個線程同時持有一個線程對象,一個嘗試去中斷線程,另一個嘗試去恢複線程,如果並發進行的話,無論調用時是否進行了同步,目標線程都有死鎖風險。正因此如此,這兩個方法已經被廢棄啦。
Java中如何保證線程安全性
並發(concurrency)一個並不陌生的詞,簡單來說,就是cpu在同一時刻執行多個任務。
而Java並發則由多線程實現的。
在jvm的世界裡,線程就像不相干的平行空間,串列在虛擬機中。(當然這是比較籠統的說法,線程之間是可以交互的,他們也不一定是串列。)
多線程的存在就是壓榨cpu,提高程序性能,還能減少一定的設計複雜度(用現實的時間思維設計程序)。
這麼說來似乎線程就是傳說中的銀彈了,可事實告訴我們真正的銀彈並不存在。
多線程會引出很多難以避免的問題, 如死鎖,臟數據,線程管理的額外開銷,等等。更大大增加了程序設計的複雜度。
但他的優點依舊不可替代。
死鎖和臟數據就是典型的線程安全問題。
簡單來說,線程安全就是: 在多線程環境中,能永遠保證程序的正確性。
只有存在共享數據時才需要考慮線程安全問題。
java內存區域:
其中, 方法區和堆就是主要的線程共享區域。那麼就是說共享對象只可能是類的屬性域或靜態域。
了解了線程安全問題的一些基本概念後, 我們就來說說如何解決線程安全問題。我們來從一個簡單的servlet示例來分析:
public class ReqCounterServlet extends HttpServlet{ private int count = 0;
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
count++;
System.out.print(“當前已達到的請求數為” + count);
}
public void doPost(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException { // ignore }
}
1. 了解業務場景的線程模型
這裡的線程模型指的是: 在該業務場景下, 可能出現的線程調用實況。
眾所周知,Servlet是被設計為單實例,在請求進入tomcat後,由Connector建立連接,再講請求分發給內部線程池中的Processor,
此時Servlet就處於一個多線程環境。即如果存在幾個請求同時訪問某個servlet,就可能會有幾個線程同時訪問該servlet對象。如圖:
線程模型,如果簡單的話,就在腦海模擬一下就好了,複雜的話就可以用紙筆或其他工具畫出來。
2. 找出共享對象
這裡的共享對象就很明顯就是ReqCounterServlet。
3. 分析共享對象的不變性條件
不變性條件,這個名詞是在契約式編程的概念中的。不變性條件保證類的狀態在任何功能被執行後都保持在一個可接受的狀態。
這裡可以引申出, 不可變對象是線程安全的。(因為不可變對象就沒有不變性條件)
不變性條件則主要由對可變狀態的修改與訪問構成。
這裡的servlet很簡單, 不變性條件大致可以歸納為: 每次請求進入時count計數必須加一,且計數必須正確。
在複雜的業務中, 類的不變性條件往往很難考慮周全。設計的世界是險惡的,只能小心謹慎,用測量去證明,最大程度地減少錯誤出現的幾率。
4. 用特定的策略解決線程安全問題。
如何解決的確是該流程的重點。目前分三種方式解決:
第一種,修改線程模型。即不在線程之間共享該狀態變數。一般這個改動比較大,需要量力而行。
第二種,將對象變為不可變對象。有時候實現不了。
第三種,就比較通用了,在訪問狀態變數時使用同步。 synchronized和Lock都可以實現同步。簡單點說,就是在你修改或訪問可變狀態時加鎖,獨佔對象,讓其他線程進不來。
這也算是一種線程隔離的辦法。(這種方式也有不少缺點,比如說死鎖,性能問題等等)
其實有一種更好的辦法,就是設計線程安全類。《代碼大全》就有提過,問題解決得越早,花費的代價就越小。
是的,在設計時,就考慮線程安全問題會容易的多。
首先考慮該類是否會存在於多線程環境。如果不是,則不考慮線程安全。
然後考慮該類是否能設計為不可變對象,或者事實不可變對象。如果是,則不考慮線程安全
最後,根據流程來設計線程安全類。
設計線程安全類流程:
1、找出構成對象狀態的所有變數。
2、找出約束狀態變數的不變性條件。
3、建立對象狀態的並發訪問管理策略。
有兩種常用的並發訪問管理策略:
1、java監視器模式。 一直使用某一對象的鎖來保護某狀態。
2、線程安全委託。 將類的線程安全性委託給某個或多個線程安全的狀態變數。(注意多個時,這些變數必須是彼此獨立,且不存在相關聯的不變性條件。)
Java中所說的線程安全是指什麼?
關於線程安全,是指當多個線程訪問同一個變數時,該變數不會因為多線程訪問產生意想不到的問題,為了避免多線程訪問的不可預知的問題,對於程序中多線程能訪問到的變數要加鎖,即加synchronized,放在同步塊中,或者對改變該變數值的方法加synchronized限制。當然jdk中自帶的一些類本身就實現了該機制,本身就是線程安全的,比如StringBuffer,Vector等。
Java的List如何實現線程安全?
解決這個問題通常有兩種方法(個人認為)
一:使用synchronized關鍵字,這個大家應該都很熟悉了,不解釋了;
二:使用Collections.synchronizedList();使用方法如下:
假如你創建的代碼如下:ListMapString,Object data=new ArrayListMapString,Object();
那麼為了解決這個線程安全問題你可以這麼使用Collections.synchronizedList(),如:
ListMapString,Object data=Collections.synchronizedList(new ArrayListMapString,Object());
其他的都沒變,使用的方法也幾乎與ArrayList一樣,大家可以參考下api文檔;
額外說下 ArrayList與LinkedList;這兩個都是介面List下的一個實現,用法都一樣,但用的場所的有點不同,ArrayList適合於進行大量的隨機訪問的情況下使用,LinkedList適合在表中進行插入、刪除時使用,二者都是非線程安全,解決方法同上(為了避免線程安全,以上採取的方法,特別是第二種,其實是非常損耗性能的)。
如何創建線程?如何保證線程安全?
線程安全等級
之前的博客中已有所提及「線程安全」問題,一般我們常說某某類是線程安全的,某某是非線程安全的。其實線程安全並不是一個「非黑即白」單項選擇題。按照「線程安全」的安全程度由強到弱來排序,我們可以將java語言中各種操作共享的數據分為以下5類:不可變、絕對線程安全、相對線程安全、線程兼容和線程對立。
1、不可變
在java語言中,不可變的對象一定是線程安全的,無論是對象的方法實現還是方法的調用者,都不需要再採取任何的線程安全保障措施。如final關鍵字修飾的數據不可修改,可靠性最高。
2、絕對線程安全
絕對的線程安全完全滿足Brian GoetZ給出的線程安全的定義,這個定義其實是很嚴格的,一個類要達到「不管運行時環境如何,調用者都不需要任何額外的同步措施」通常需要付出很大的代價。
3、相對線程安全
相對線程安全就是我們通常意義上所講的一個類是「線程安全」的。
它需要保證對這個對象單獨的操作是線程安全的,我們在調用的時候不需要做額外的保障措施,但是對於一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。
在java語言中,大部分的線程安全類都屬於相對線程安全的,例如Vector、HashTable、Collections的synchronizedCollection()方法保證的集合。
4、線程兼容
線程兼容就是我們通常意義上所講的一個類不是線程安全的。
線程兼容是指對象本身並不是線程安全的,但是可以通過在調用端正確地使用同步手段來保證對象在並發環境下可以安全地使用。Java API中大部分的類都是屬於線程兼容的。如與前面的Vector和HashTable相對應的集合類ArrayList和HashMap等。
5、線程對立
線程對立是指無論調用端是否採取了同步錯誤,都無法在多線程環境中並發使用的代碼。由於java語言天生就具有多線程特性,線程對立這種排斥多線程的代碼是很少出現的。
一個線程對立的例子是Thread類的supend()和resume()方法。如果有兩個線程同時持有一個線程對象,一個嘗試去中斷線程,另一個嘗試去恢複線程,如果並發進行的話,無論調用時是否進行了同步,目標線程都有死鎖風險。正因此如此,這兩個方法已經被廢棄啦。
二、線程安全的實現方法
保證線程安全以是否需要同步手段分類,分為同步方案和無需同步方案。
1、互斥同步
互斥同步是最常見的一種並發正確性保障手段。同步是指在多線程並發訪問共享數據時,保證共享數據在同一時刻只被一個線程使用(同一時刻,只有一個線程在操作共享數據)。而互斥是實現同步的一種手段,臨界區、互斥量和信號量都是主要的互斥實現方式。因此,在這4個字裡面,互斥是因,同步是果;互斥是方法,同步是目的。
在java中,最基本的互斥同步手段就是synchronized關鍵字,synchronized關鍵字編譯之後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個位元組碼質量,這兩個位元組碼指令都需要一個reference類型的參數來指明要鎖定和解鎖的對象。
此外,ReentrantLock也是通過互斥來實現同步。在基本用法上,ReentrantLock與synchronized很相似,他們都具備一樣的線程重入特性。
互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題,因此這種同步也成為阻塞同步。從處理問題的方式上說,互斥同步屬於一種悲觀的並發策略,總是認為只要不去做正確地同步措施(例如加鎖),那就肯定會出現問題,無論共享數據是否真的會出現競爭,它都要進行加鎖。
2、非阻塞同步
隨著硬體指令集的發展,出現了基於衝突檢測的樂觀並發策略,通俗地說,就是先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了;如果共享數據有爭用,產生了衝突,那就再採用其他的補償措施。(最常見的補償錯誤就是不斷地重試,直到成功為止),這種樂觀的並發策略的許多實現都不需要把線程掛起,因此這種同步操作稱為非阻塞同步。
非阻塞的實現CAS(compareandswap):CAS指令需要有3個操作數,分別是內存地址(在java中理解為變數的內存地址,用V表示)、舊的預期值(用A表示)和新值(用B表示)。CAS指令執行時,CAS指令指令時,當且僅當V處的值符合舊預期值A時,處理器用B更新V處的值,否則它就不執行更新,但是無論是否更新了V處的值,都會返回V的舊值,上述的處理過程是一個原子操作。
CAS缺點:
ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。
ABA問題的解決思路就是使用版本號。在變數前面追加版本號,每次變數更新的時候把版本號加一,那麼A-B-A就變成了1A-2B-3C。JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置為給定的更新值。
3、無需同步方案
要保證線程安全,並不是一定就要進行同步,兩者沒有因果關係。同步只是保證共享數據爭用時的正確性的手段,如果一個方法本來就不涉及共享數據,那它自然就無需任何同步操作去保證正確性,因此會有一些代碼天生就是線程安全的。
1)可重入代碼
可重入代碼(ReentrantCode)也稱為純代碼(Pure Code),可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼,而在控制權返回後,原來的程序不會出現任何錯誤。所有的可重入代碼都是線程安全的,但是並非所有的線程安全的代碼都是可重入的。
可重入代碼的特點是不依賴存儲在堆上的數據和公用的系統資源、用到的狀態量都是由參數中傳入、不調用 非可重入的方法等。
(類比:synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個線程得到一個對象鎖後,再次請求此對象鎖時時可以再次得到該對象的鎖)
2)線程本地存儲
如果一段代碼中所需的數據必須與其他代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行?如果能保證,我們就可以把共享數據的可見範圍限制在同一個線程之內。這樣無需同步也能保證線程之間不出現數據的爭用問題。
符合這種特點的應用並不少見,大部分使用消費隊列的架構模式(如「生產者-消費者」模式)都會將產品的消費過程盡量在一個線程中消費完。其中最重要的一個應用實例就是經典的Web交互模型中的「一個請求對應一個伺服器線程(Thread-per-Request)」的處理方式,這種處理方式的廣泛應用使得很多Web伺服器應用都可以使用線程本地存儲來解決線程安全問題。
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/182028.html