摘要:線程被稱為輕量級進程。在大多數操作系統中,線程都是最基本的調度單位。在多線程程序中,,還存在由于使用多線程而引入的其他問題。由于多線程訪問無狀態對象的行為不會影響到其他線程中操作的正確性,因此無狀態對象一定是線程安全的。
概述
最近遇到了些并發的問題,恰巧也有朋友問我類似的問題,無奈并發基礎知識過弱,只大概了解使用一些同步機制和并發工具包類,沒有形成一個完整的知識體系,并不能給出一個良好的解決方案。深知自己就是個弟弟,趁著周末有空,就趕緊把之前買的并發編程實戰拿起來,擦擦灰,惡補一下....
并發簡史在介紹并發前,我們先來簡單的了解下計算機的發展歷史。早期的計算機是不包含操作系統的,他們可以使用計算機上所有的資源,計算機從頭到尾也就只執行著一個程序。在這種裸機環境下,編寫和運行程序將變的非常麻煩,并且只運行一個程序對于計算機來說也是一種嚴重的浪費。為了解決這個問題,操作系統閃亮登場,計算機每次都能運行多個程序,每個程序都是一個多帶帶的進程。操作系統為每一個進程分配各種資源,比如:內存、文件句柄等。如果需要的話,不同的進程之間可以通過通信機制來交換數據。
操作系統的出現,主要給我們解決了這幾個問題,資源利用率的提高,程序之間的公平性和便利性。
資源利用率
有些情況下,程序必須等待某個外部操作完成才能繼續進行。比如當我們向計算機復制數據的時候,此時只有io在工作,如果在等待復制的時間,計算機可以運行其他程序,無疑大大提高了資源的利用率。
公平性
操作系統常見的一種方式就是通過粗粒度的時間分片來使用戶和程序能共享計算機資源,而不是一個程序從頭運行到尾,然后再啟動下一個程序。想一想,你可以用著自己的個人pc,打著游戲,聽著歌,和女朋友聊著天,計算機資源會來回切換,只不過因為速度很快,給我們的感覺就像是同時發生一樣,這一切都要歸功于操作系統的調配。
便利性
一般來說,在計算多個任務時,應該編寫多個程序,每個程序在執行一個任務時并在需要時進行通信,這比只編寫一個程序來計算所有任務更容易實現。
線程的出現和進程的出現是一個道理的,只不過一個調配的是一個進程內的資源問題,另一個是調配一臺計算機之間的資源。進程允許存在多個線程,并且線程之間會共享進程范圍內的資源(內存和文件句柄),但每個線程都有自己的程序計數器、棧等,而且同一個程序的多個線程可以同時被調度到多個cpu上運行。
線程被稱為輕量級進程。在大多數操作系統中,線程都是最基本的調度單位。如果沒有統一的協同機制,線程將彼此獨立運行,由于同一個進程上的所有線程都將共享進程的內存空間,它們將訪問相同的變量并在同一個堆上分配對象,這就需要一個更細粒度的數據共享機制,不然將造成不可預測的后果。
線程可以充分發揮多處理器的強大能力
避免單個線程阻塞而導致整個程序停頓
異步事件的簡化處理
線程的風險安全性問題
class ThreadSafeTest{ static int count; public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(1); for (int i=0;i<100;i++){ new Thread(new Runnable() { @Override public void run() { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } for (int x=0;x<100;x++){ count++; } } }).start(); } countDownLatch.countDown(); Thread.sleep(5000); System.out.println("count:"+count); } } 輸出結果:count:9635
運行結果:count:9955.我們的期待結果是10000,并且我們多次的運行結果還可能不一樣。這個問題主要在于count++并不是一個原子操作,它可以分為讀取count,count+1和計算結果寫回。如果在缺少同步的情況下,我們無法保證多線程情況下結果的正確性.
活躍性問題
安全性的含義是永遠不會發生錯誤的事情,而活躍性的含義將是正確的事情最終會發生。當某個操作無法繼續執行下去,就會發生活躍性的問題。在穿行程序中,無意中造成的無限循環就是活躍性問題之一。此外分別還有死鎖、饑餓以及活鎖問題。
死鎖:線程A在等待線程B釋放其擁有的資源,而線程B在等待線程A釋放其擁有的資源,這樣僵持不下,那么線程A、B就會永遠等下去。
饑餓:最常見的饑餓問題就是CPU時鐘周期問題。如果在java程序中存在持有鎖時執行一些無法結束的結構(無限循環或者是等待某個資源發生阻塞),那么很可能將導致饑餓,因為其他需要這個鎖的線程將無法得到它。
活鎖:活鎖不會阻塞線程,但也不能繼續執行。假如程序不能正確的執行某個操作,因為事務回滾,并將其放到隊列的頭部。由于這條事務回滾的消息被放回到隊列頭部,處理器將反復調用,并返回相同的結果。
性能問題
性能問題和活躍性問題是密切相關的。活躍性意味著某件正確的事情最終會發生,但是我們一般更希望正確的事情盡快的發生。性能問題包括多個方面:服務時間過長、響應不靈敏、吞吐率過低、資源消耗過高、和可伸縮性較差等。在多線程程序中,,還存在由于使用多線程而引入的其他問題。在多線程程序中,當線程調度器臨時掛起活躍線程并轉而運行另一個線程時,就會頻繁的出現上下文切換操作,這種操作將帶來極大的開銷(保存和恢復執行上下文,丟失局部性,并且CPU時鐘周期將更多地花費在線程調度上而不是線程運行上)。并且,當多個線程共享數據時,必須使用同步機制,而這些機制往往會抑制某些編譯器優化,使內存緩沖區的數據無效,以及增加共享內存總線的同步流量。
什么是線程安全性?
當多個線程訪問某個類時,不管運行時環境采用何種調度方式或者這些線程將如何交替執行,并且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那么就稱這個類是線程安全的。
從ThreadSafeTest例子我們可以清楚線程安全性可能是非常復雜的,再沒有充足同步的情況下,多個線程中的操作執行順序是不可預測的,可能會發生奇怪的結果。
無狀態對象一定是線程安全的**
相信大家都對servlet有過了解,它是一個框架,其作用大概就是接收請求,處理參數,分發請求和返回結果。servlet是線程安全的,因為它是無狀態的。我們來自定義個servlet:
public class NoStateCalculate implements Servlet { @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { //解析數據樣本,計算結果calculate BigInteger calculate = calculate(req); Integer data= getData(calculate); res.getOutputStream().print(data); } }
這個servlet從請求中提取參數并計算結果calculate,然后去數據庫中查詢對應的數據data,最終將其寫入到輸出中。它是一個無狀態的類,它不包含任何域,也不包含其他任何域的引用,所有的臨時狀態都存在于線程棧上的局部變量表中,并且只能由正在執行的線程訪問,線程之間不會相互影響,因此可以說線程之間沒有共享狀態。由于多線程訪問無狀態對象的行為不會影響到其他線程中操作的正確性,因此無狀態對象一定是線程安全的。
原子性
ThreadSafeTest例子并不是一個線程安全的例子,原因是將有100個線程同時調用count++,而count++又不是一個原子性的操作,其結果將可能是不正確的。
競態條件:當某個計算的正確性取決于多個線程的交替執行時序時,那么就會發生競態條件(正確的結果依賴于運氣)。
復合操作:count++就是一組復合操作,要避免競態條件問題,就必須將操作原子化。
讓我們對ThreadSafeTest進行改造,使用jdk提供的原子變量類AtomicInteger:
public class ThreadSafeTest { static AtomicInteger count =new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { final CountDownLatch countDownLatch = new CountDownLatch(1); for (int i=0;i<100;i++){ new Thread(new Runnable() { @Override public void run() { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } for (int x=0;x<100;x++){ count.incrementAndGet(); } } }).start(); } countDownLatch.countDown(); Thread.sleep(5000); System.out.println("count:"+count); } }
通過AtomicInteger可以將單個整數變量的操作原子化,使其變成線程安全的。當共享狀態變量多于一個時,這種機制就不能解決問題了,應該通過鎖機制保證操作的原子性。
加鎖機制
內置鎖:java提供了synchronized內置鎖來支持原子性。它可以修飾在方法上,代碼塊上,其中同步代碼塊和普通方法的鎖就是方法調用所在的對象,而靜態方法的synchronized則以當前的Class對象作為鎖。線程在進入同步代碼之前會自動獲得鎖,退出同步代碼塊時自動釋放鎖。synchronized同時也是一種互斥鎖,最多只有一個線程持有這種鎖,所以它可以保證同步代碼操作的原子性。
重入鎖:當某個線程請求一個由其他線程持有的鎖時,發出的請求就會阻塞。然而內置鎖是可重入的,因此如果某個線程試圖獲得一個已經由他自己的持有的鎖,那么這個請求就會成功。"重入"意味著獲取鎖的操作的粒度是線程,重入的一種實現方式就是為每個鎖關聯一個獲取計數值和一個所有者線程。當數值為0時就代表鎖沒有被任何線程獲得,當一個線程獲得鎖時,計數器會加1,當同一個線程再次獲得鎖時,將會在加1,以此來實現鎖的重入功能。
用鎖來保護狀態:
活躍性和性能
java的語法糖提供了一個內置鎖synchronized,它可以很好地保證同步代碼塊只有一個線程執行。但是如果synchronized使用的不當,將會帶來嚴重的活躍性和性能問題。其對應的優化技術有很多,避免死鎖,減小鎖粒度,鎖分段等等都可有效的解決活躍性問題和性能問題(留到以后再介紹)。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/69620.html
摘要:進程可創建多個線程來執行同一程序的不同部分。就緒等待線程調度。運行線程正常運行阻塞暫停運行,解除阻塞后進入狀態重新等待調度。消亡線程方法執行完畢返回或者異常終止。多線程多的情況下,依次執行各線程的方法,前頭一個結束了才能執行后面一個。 淺談Python多線程 作者簡介: 姓名:黃志成(小黃)博客: 博客 線程 一.什么是線程? 操作系統原理相關的書,基本都會提到一句很經典的話: 進程...
摘要:線程池的作用降低資源消耗。通過重復利用已創建的線程降低線程創建和銷毀造成的資源浪費。而高位的部分,位表示線程池的狀態。當線程池中的線程數達到后,就會把到達的任務放到中去線程池的最大長度。默認情況下,只有當線程池中的線程數大于時,才起作用。 線程池的作用 降低資源消耗。通過重復利用已創建的線程降低線程創建和銷毀造成的資源浪費。 提高響應速度。當任務到達時,不需要等到線程創建就能立即執行...
摘要:比如需要用多線程或分布式集群統計一堆用戶的相關統計值,由于用戶的統計值是共享數據,因此需要保證線程安全。如果類是無狀態的,那它永遠是線程安全的。參考探索并發編程二寫線程安全的代碼 線程安全類 保證類線程安全的措施: 不共享線程間的變量; 設置屬性變量為不可變變量; 每個共享的可變變量都使用一個確定的鎖保護; 保證線程安全的思路: 1. 通過架構設計 通過上層的架構設計和業務分析來避...
閱讀 3416·2023-04-26 02:41
閱讀 2472·2023-04-26 00:14
閱讀 2891·2021-08-11 10:22
閱讀 1295·2019-12-27 11:38
閱讀 3583·2019-08-29 18:34
閱讀 2390·2019-08-29 12:13
閱讀 2964·2019-08-26 18:26
閱讀 1878·2019-08-26 16:49