日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

java并发编程实践(2)线程安全性

發布時間:2023/12/3 编程问答 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java并发编程实践(2)线程安全性 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
【0】README 0.0)本文部分文字描述轉自:“java并發編程實戰”, 旨在學習“java并發編程實踐(2)線程安全性” 的相關知識; 0.1)幾個術語(terms) t1)對象的狀態:是指存儲在狀態變量中的數據; t2)共享:意味著變量可以有多個線程同時訪問; t3)可變:意味著變量的值在生命周期內可以發送變化; Attention)我們將像討論代碼那樣來討論線程安全性,但更側重于如何防止在數據上發送不受控的并發訪問; 0.2)一個對象是否需要是線程安全的,取決于它是否被多個線程訪問: 要使得對象是線程安全的,需要采用同步機制來協同對對象可變狀態的訪問; 0.3)當多個線程訪問某個狀態變量并且其中有一個線程執行寫入操作時,必須采用同步機制來協同這些線程對變量的訪問;java中的主要同步機制是關鍵字synchronized,它提供了一種獨占的加鎖方式,但“同步”術語還包括volatile類型的變量,顯式鎖以及原子變量; Conclusion)同步術語有4種: synchronized關鍵字;volatile類型的變量;顯式鎖;原子變量;(干貨——同步術語有4種) 0.4)如果當多個線程訪問同一個可變的狀態變量時沒有使用合適的同步,那么程序就會出現錯誤。有三種方式可以修改這個問題(ways): way1)不在線程之間共享該狀態變量; way2)將狀態變量修改為不可變的變量; way3)在訪問狀態變量時使用同步;
【1】什么是線程安全性 1)在線程安全性的定義中,最核心的概率是正確性; 2)正確性定義:某個類的行為與其規范完全一致;(我們將現場的正確性近似定義為所見即所知) 3)線程安全性定義:當多個線程訪問某個類時,這個類始終都能表現出正確的行為,那么就稱這個類是線程安全的 ; 4)無狀態對象:該對象既不包含任何域,也不包含任何對其他類中域的引用,計算過程中的臨時狀態僅存儲在線程棧上的局部變量中,并且只能由正在執行的線程訪問;(干貨——無狀態對象是線程安全的)

【2】原子性 看個荔枝)計數器 public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {private long count = 0;public long getCount() {return count;}public void service(ServletRequest req, ServletResponse resp) {BigInteger i = extractFromRequest(req);BigInteger[] factors = factor(i);++count; // highlight line.encodeIntoResponse(resp, factors);} } 對以上代碼的分析(Analysis):
A1)它包含三個獨立的操作: 讀取count的值;將值加1;然后將計算結果寫入count;(干貨——這是一個讀取——修改——寫入的操作序列,并且其結果依賴于以前的狀態) A2)圖1.1給出了兩個線程在沒有同步的case下同時對一個計數器執行遞增操作時發生的情況,這不是線程安全的;

【2.1】競態條件 1)intro:當某個計算的正確性取決于多個線程的交替執行時序時,那么就會發生競態條件;換句話說,就是正確的結果要取決于運氣。最常見的競態條件類型是“先檢查后執行(check-then-act)”操作,即通過一個可能失效的觀測結果來決定下一步的動作;(干貨——競態條件它是一個條件,當...的時候,當某個計算的正確性取決于多個線程的交替執行時序時就產生了競態條件) 2)先檢查后執行的概念:競態條件的本質——基于一種可能失效的觀察結果來做出判斷或者執行某個計算。這種類型的競態條件稱為“先檢查后執行”,首先觀察到某個條件為真(如文件X不存在),然后根據這個觀察結果采用相應的動作(創建文件X),但事實上,在你觀察到這個結果以及開始創建文件之間,觀察結果可能變得無效了(另一個線程在這期間創建了文件X),從而導致各種問題(數據被覆蓋,文件被破壞等);(干貨——先檢查后執行的概念)
【2.2】實例:延遲初始化種的競態條件 1)使用先檢查后執行的一種常見case 就是 延遲初始化:延遲初始化的目的是將對象的初始化操作推遲到設計被使用時才進行,同時要確保只被初始化一次;(干貨——引入延遲初始化) 看個荔枝)延遲初始化中的競態條件(不要這么做) <pre name="code" class="java">public class LazyInitRace {private ExpensiveObject instance = null;public ExpensiveObject getInstance() {if (instance == null)instance = new ExpensiveObject();return instance;} } class ExpensiveObject { } 對以上代碼的分析(Analysis): A1)在LazyInitRace?中包含了一個競態條件,它可能會破壞這個類的正確性; A2)假設線程A 和 線程B 同時執行getInstace方法:A看到instance為空, 因此創建一個新的ExpensiveObject?實例;B同樣需要判斷instance是否為空。此時的instance是否為空,要取決于不可預測的時序,包括線程的調度方式,以及A需要花多長時間來初始化ExpensiveObject?并設置instance;如果B檢查到 instance為空, 那么在兩次調用getInstance方法時可能會得到不同的結果,即使getInstance通常被認為是返回相同的實例; Attention)一種競態條件: 讀取——修改——寫入這種操作(如count++, 遞增一個計數器);
【2.3】復合操作 1)LazyInitRace?類包含一組需要以原子方式執行的操作。要避免競態條件問題,就必須在某個線程修改該變量時,通過某個方式防止其他線程使用這個變量,從而確保其他線程只能在修改操作完成之前或之后讀取和修改狀態,而不是在修改狀態的 過程中;(干貨——如何避免競態條件問題) Attention)原子操作定義:假定有兩個操作O1 和 O2,如果從執行操作O1 的線程T1來看,當另一個線程T2執行操作O2時,要么將操作O2全部執行完,要么完全不執行操作O2,那么操作O1 和 操作O2 對彼此來說是原子的。原子操作是指,對于訪問同一個狀態的所有操作(包括該操作本身)來說, 這個操作是一個以原子方式執行的操作;(干貨——原子操作定義) 2)復合操作:我們將“先檢查后修改”以及“讀取——修改——寫入”等操作統稱為復合操作:包含了一組必須以原子方式執行的操作以確保線程安全性; 3)使用一個現有的線程安全類來修改?UnsafeCountingFactorizer?得到?CountingFactorizer? public class CountingFactorizer extends GenericServlet implements Servlet { // code2.2.3private final AtomicLong count = new AtomicLong(0); //highlight line. safe thread class.public long getCount() { return count.get(); }public void service(ServletRequest req, ServletResponse resp) {BigInteger i = extractFromRequest(req);BigInteger[] factors = factor(i);count.incrementAndGet(); // highlight line.encodeIntoResponse(resp, factors);} } 對以上代碼的分析(Analysis):
A1)在 java.util.concurrent.atomic包中包含了一些原子變量類,用于實現在數值和對象引用上的原子狀態轉換; A2)通過用AtomicLong 來代替long類型的計數器,能夠確保所有對計數器狀態的訪問操作都是原子性的; A3)由于servlet的狀態就是計數器的狀態,并且計數器是線程安全的,因此這里的servlet也是線程安全的; Attention)在實際case中,應該盡可能使用現有的線程安全對象(如AcomicLong)來管理類的狀態;
【3】加鎖機制(java中用于確保原子性的內置機制) 1)requirement:假設我們想提升servlet的性能,將最近的計算結果緩存起來,當兩個連續的請求對相同的數值進行因式分解時,可以直接使用上一次的計算結果,而無須重新計算。要實現該緩存策略,需要保存兩個狀態:最近執行因式分解的數值以及分解結果; 2)代碼2.3 通過AtomicLong以線程安全的方式來管理計數器的狀態,那么,在這里是否也可以使用類似的 AtomicReference來管理最近執行因式分解的數值及其分解結果嗎? public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {private final AtomicReference<BigInteger> lastNumber= new AtomicReference<BigInteger>(); //被分解的數值private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>(); //分解后的因子public void service(ServletRequest req, ServletResponse resp) {BigInteger i = extractFromRequest(req);if (i.equals(lastNumber.get()))encodeIntoResponse(resp, lastFactors.get());else {BigInteger[] factors = factor(i);lastNumber.set(i); //highlight line.lastFactors.set(factors); //highlight line.encodeIntoResponse(resp, factors);}} } 對以上代碼的分析(非安全的)Analysis: A1)在線程安全性的定義中要求,多個線程之間的操作無論采用何種執行時序或交替方式,都要保證不變性條件不被破壞; A2)UnsafeCachingFactorizer?的不變性條件之一是:在 lastFactors 中緩存的因數之積應該等于在 lastNumber 中緩存的數值;所以當更新某個變量時,需要在同一個原子操作中對其他變量同時進行更新;如第1次請求分解12, 而第2次請求分解20,第3次請求分解20;當請求分解20的時候,lastNumber變了,這就會引起lastFactors 改變; A3)在使用原子引用(AtomicReference)的case下,盡管對set方法的每次調用都是原子的,但仍然無法同時更新lastNumber 和 lastFactors。如果只修改了其中一個變量,那么在這兩次修改操作之間,其他線程將發現不變性條件被破壞了; A4)而且,我們也不能保證會同時獲取兩個值:在線程A獲取這兩個值的過程中,線程B 可能修改了它們,這樣線程A 也會發現不變性條件被破壞了; Attention) 要保持狀態的一致性,就需要在單個原子操作中更新所有相關的狀態變量;(干貨——要保持狀態的一致性,就需要在單個原子操作中更新所有相關的狀態變量)

【3.1】內置鎖 1)intro to 同步代碼塊:java提供了一種內置的鎖機制來支持原子性——同步代碼塊; 2)同步代碼塊分為兩部分:一個是作為鎖的對象引用,一個是作為由這個鎖保護的代碼塊; 3)以關鍵字synchronized來修飾的方法就是一種橫跨方法體的同步代碼塊,其中該同步代碼塊的鎖就是方法調用所在的對象。靜態的synchronized方法以Class對象作為鎖;(干貨——同步代碼塊和鎖的定義) synchronized(lock) { // 訪問或修改由鎖保護的共享狀態 } 4)每個java對象都可以用作一個實現同步的鎖,這些鎖被稱為內置鎖或監視器鎖。線程在進入同步代碼塊之前會自動獲得鎖,并且在退出同步代碼塊時自動釋放鎖,而無論是通過正常的控制路徑退出,還是通過從代碼塊拋出異常退出。獲得內置鎖的唯一途徑就是進入由這個鎖保護的同步代碼塊或方法;(干貨——獲得內置鎖的唯一途徑就是進入由這個鎖保護的同步代碼塊或方法,且每次只有一個線程執行內置鎖保護的代碼塊) 5)并發環境中的原子性與事務應用程序中的原子性有著相同的含義:一組語句作為一個不可分割的單元被執行;任何一個執行同步代碼塊的線程,都不可能看到有其他線程正在執行由同一個鎖保護的同步代碼塊; 6)下面是UnsafeCachingFactorizer?引入同步代碼塊(synchronized關鍵字)后的SynchronizedFactorizer?代碼: public class SynchronizedFactorizer extends GenericServlet implements Servlet {@GuardedBy("this") private BigInteger lastNumber;@GuardedBy("this") private BigInteger[] lastFactors;public synchronized void service(ServletRequest req,ServletResponse resp) {BigInteger i = extractFromRequest(req);if (i.equals(lastNumber))encodeIntoResponse(resp, lastFactors);else {BigInteger[] factors = factor(i);lastNumber = i;lastFactors = factors;encodeIntoResponse(resp, factors);}} 對以上代碼的分析(Analysis): A1)用關鍵字synchronized來修飾方法service()方法,因此在同一時刻只有一個線程可以執行service方法,這種方法過于極端了,因為多個clients 無法同時使用因式分解,服務的響應性能降低; A2)所以在 synchronized關鍵字修改service()方法之后,這就變成一個性能問題,而不是線程安全問題了;(干貨——非線程安全轉為線程安全但卻帶來了性能問題)
【3.2】重入(內置鎖是可重入的) 1)當某個線程請求一個由其他線程持有的鎖時,發出請求的線程就會阻塞。然而,由于內置鎖是可以重入的,因此如果某個線程試圖獲取一個已經由它持有的鎖,那么這個請求就會成功; 2)重入的概念:“重入”意味著獲取鎖的操作的粒度是線程,而不是調用;重入的一種實現方法是,為每個鎖關聯一個獲取計數值和一個所有者線程。。當計數值為0時,這個鎖就被認為是沒有被任何線程所持有的。當線程請求一個未被持有的鎖時,JVM 將記下鎖的持有者,并且將獲取計數值設置為1.如果同一個線程再次獲取這個鎖,計數值將遞增,而當線程退出同步代碼塊時,計數器會相應地遞減。當計數值減為0時,這個鎖將被釋放;(干貨——重入的原理) 3)重入進一步提升了加鎖行為的封裝性 看個荔枝)子類改寫了父類的 synchronized方法,然后調用父類的方法,此時如果沒有可重入的鎖,那么這段代碼將產生死鎖; 3.1)產生死鎖的原因:因為每個doSth方法在執行前都會獲得 Widget上的鎖。然而,如果內置鎖不是可重入的,那么在調用 super.doSth時將無法獲得 Widget上的鎖,因為這個鎖已經被持有,從而線程將永遠停頓下去,等待一個永遠也無法獲取的鎖。重入則避免了這種死鎖case的發生; public class Widget {public synchronized void doSth(){...} } public class LoggineWidget extends Widget {public synchronized void doSth() {super.doSth();} }【4】用鎖來保護狀態 1)狀態變量是由這個鎖保護的:對于可能被多個線程同時訪問的可變狀態變量,在訪問它時都需要持有同一個鎖,在這種case下,稱狀態變量是由這個鎖保護的; 2)當某個變量由鎖來保護時,意味著在每次訪問這個變量時都需要首先獲得鎖,這樣就確保在同一時刻只有一個線程可以訪問這個變量。當類的不變性條件涉及多個狀態變量時,那么還有另外一個需求:在不變性條件中的每個變量都必須由同一個鎖來保護;因此可以在單個原子操作中訪問或更新這些變量,從而確保不變性條件不被破壞; Attention)對于每個包含多個變量的不變形條件,其中涉及的所有變量都需要由同一個鎖來保護;
3)如果同步可以避免競態條件的問題,為什么不在每個方法聲明時都使用關鍵字synchronized??事實上,如果不加區別地濫用 synchronized,可能導致程序中出現過多的同步;將每個方法都作為同步方法還可能導致活躍性問題或性能問題;
【5】活躍性與性能 1)參見“3.1”中的SynchronizedFactorizer?,該類的service方法是一個synchronized方法,因此每次只有一個線程可以執行。這就背離了Servlet框架的初衷,即servlet需要能同時處理多個請求,這在負載過高的case下 將給用戶帶來糟糕的體驗; 2)下圖給出了當多個請求同時到達 因式分解時發生的case: 這些請求將排隊等待處理。我們將這種web應用程序稱為“不良并發程序”,因為可同時調用的數量,不僅受到可用處理資源的限制,還受到應用程序本身結構的限制;
3)幸運的是:通過縮小同步代碼塊的作用范圍,我們很容易做到既確保servlet的并發性,同時又維護線程安全性;應該盡量將不影響共享狀態且執行時間過長的操作從同步代碼塊中分離出去,從而在這些操作的執行過程中,其他線程可以訪問共享狀態; 4)看個荔枝:將SynchronizedFactorizer修改為?CachedFactorizer,該代碼使用兩個獨立的同步代碼塊,每個同步代碼塊都只包含一小段代碼。其中一個同步代碼塊負責保護判斷是否只需要返回緩存結果的“先檢查后執行”操作序列,另一個同步代碼塊則負責確保對 緩存的數值和因式分解結果進行同步更新;(干貨——同步代碼塊包括synchronized代碼塊和synchronized修飾的方法) public class CachedFactorizer extends GenericServlet implements Servlet {@GuardedBy("this") private BigInteger lastNumber;@GuardedBy("this") private BigInteger[] lastFactors;@GuardedBy("this") private long hits;@GuardedBy("this") private long cacheHits;public synchronized long getHits() {return hits;}public synchronized double getCacheHitRatio() {return (double) cacheHits / (double) hits;}public void service(ServletRequest req, ServletResponse resp) {BigInteger i = extractFromRequest(req);BigInteger[] factors = null;synchronized (this) {++hits;if (i.equals(lastNumber)) {++cacheHits;factors = lastFactors.clone();}}if (factors == null) {factors = factor(i);synchronized (this) {lastNumber = i;lastFactors = factors.clone();}}encodeIntoResponse(resp, factors);} Attention) A1)通常,在簡單性與性能之間存在著相互制約因素。當實現某個同步策略時,一定不要盲目地為了性能而犧牲簡單性(這可能會破壞安全性) A2)當執行時間較長的計算或可能無法快速完成的操作時(例如,網絡IO或控制臺 IO),一定不要持有鎖;

總結

以上是生活随笔為你收集整理的java并发编程实践(2)线程安全性的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。