並發與並行
- 並發:是指在某個時間段內,多任務交替的執行任務。當有多個線程在操作時,把CPU運行時間划分成若干個時間段,再將時間段分配給各個線程執行。 在一個時間段的線程代碼運行時,其它線程處於掛起狀。
- 並行:是指同一時刻同時處理多任務的能力。當有多個線程在操作時,cpu同時處理這些線程請求的能力。
所以在並發環境下,程序的封閉性被打破,出現以下特點:
- 並發程序之間有相互制約的關系。直接制約體現為一個程序需要另一個程序的計算結果;間接體現為多個程序競爭共享資源,如處理器、緩沖區等。
- 並發程序的執行過程是斷斷續續的。程序需要記憶現場指令及執行點。
- 當並發數設置合理並且CPU擁有足夠的處理能力時,並發會提高程序的運行效率。
在並發環境中,當一個對象可以被多個線程訪問到時,會造成該對象可以被任何訪問到的線程進行修改,從而出現數據不一致的情況。所以提出線程安全的概念。
線程基本概念介紹
線程與進程
進程:每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大的開銷,一個進程包含1--n個線程。(進程是資源分配的最小單位) 。簡單講進程就是在某種程度上相互隔離的、獨立運行的程序。
線程:同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換開銷小。(線程是cpu調度的最小單位)
線程和進程一樣分為五個階段:創建、就緒、運行、阻塞、終止。
- 創建: 新創建了一個線程對象,還未調用start()方法。 如 Thread thread = new Thread();
- 就緒: 線程對象創建后,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中 獲取cpu 的使用權 。
- 運行: 運行狀態(runnable)的線程獲得了cpu 時間片(timeslice) ,執行程序代碼。
- 阻塞(Blocked):線程被阻塞了,“阻塞狀態”與“等待狀態”的區別是:“阻塞狀態”在等待着獲取到一個排它鎖,這個事件將在另外一個線程放棄這個鎖的時候發生;而“等待狀態”則是在等待一段時間,或者喚醒動作的發生。在程序等待進入同步區域(synchronized)的時候,線程將進入這種狀態。
(一). 等待阻塞: 運行(running) 的線程執行o.wait()方法,JVM會把該線程放入等待隊列(waitting queue)中。 (二). 同步阻塞: 運行(running) 的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池(lock pool)中。 (三). 其他阻塞: 運行(running) 的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入 可運行(runnable) 狀態。
- 等待: 進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)。
阻塞:當一個線程試圖獲取一個內部的對象鎖(非java.util.concurrent庫中的鎖),而該鎖被其他線程持有,則該線程進入阻塞狀態。 等待:當一個線程等待另一個線程通知調度器一個條件時,該線程進入等待狀態。例如調用:Object.wait()、Thread.join()以及等待Lock或Condition。
- 超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間后自行返回。
- 終止(TERMINATED):表示該線程已經執行完畢。
線程安全
線程安全:多個線程訪問某個類,這個類始終都能表現出正確的行為。可以理解為一個對象可以完全的被多個線程同時使用我們稱之為線程安全的。
線程安全等級
按照線程安全的"安全程度"由強至弱來排序,我們可以將java語言中的各種操作共享數據的分為五類: 不可變、絕對線程安全、相對線程安全、線程兼容和線程對立。
- 不可變
在java語言中,不可變的對象一定是線程安全的,無論是對象的方法實現還是方法的調用者,都不需要再采取任何的線程安全保障措施。如final關鍵字修飾的數據不可修改,可靠性最高。 - 絕對線程安全
絕對的線程安全完全滿足BrianGoetZ給出的線程安全的定義,這個定義其實是很嚴格的,一個類要達到“不管運行時環境如何,調用者都不需要任何額外的同步措施”通常需要付出很大的代價。在javaAPI中標注自己是線程安全的類,大多數都不是絕對線程安全的。如 Vector hashTable等。 - 相對線程安全
相對線程安全就是我們通常意義上所講的一個類是“線程安全”的。 它需要保證對這個對象單獨的操作是線程安全的,我們在調用的時候不需要做額外的保障措施,但是對於一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。在java語言中,大部分的線程安全類都屬於相對線程安全的,例如Vector、HashTable、Collections的synchronizedCollection()方法保證的集合。 - 線程兼容
線程兼容就是我們通常意義上所講的一個類不是線程安全的。線程兼容是指對象本身並不是線程安全的,但是可以通過在調用端正確地使用同步手段來保證對象在並發環境下可以安全地使用。Java API中大部分的類都是屬於線程兼容的。如與前面的Vector和HashTable相對應的集合類ArrayList和HashMap等。 - 線程對立
線程對立是指無論調用端是否采取了同步錯誤,都無法在多線程環境中並發使用的代碼。由於java語言天生就具有多線程特性,線程對立這種排斥多線程的代碼是很少出現的。 一個線程對立的例子是Thread類的supend()和resume()方法。如果有兩個線程同時持有一個線程對象,一個嘗試去中斷線程,另一個嘗試去恢復線程,如果並發進行的話,無論調用時是否進行了同步,目標線程都有死鎖風險。正因此如此,這兩個方法已經被廢棄啦。
如何實現線程安全
實現線程安全通過是否需要同步分為兩大類;首先解釋下什么是同步。
同步: 指多個線程並發訪問共享數據時,保證共享數據在同一個時刻只能被一個線程使用。
- 互斥同步
實現同步的方法一般是互斥。如臨界區、互斥量、信號量等。因此互斥是因,同步是果;互斥是方法、同步是目的。
Java互斥手段:synchronized、JUC下的ReentrantLock。 - 非阻塞同步
隨着硬件指令集的發展,出現了基於沖突檢測的樂觀並發策略,通俗地說,就是先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了;如果共享數據有爭用,產生了沖突,那就再采用其他的補償措施。(最常見的補償錯誤就是不斷地重試,直到成功為止),這種樂觀的並發策略的許多實現都不需要把線程掛起,因此這種同步操作稱為非阻塞同步。 非阻塞的實現CAS(compareandswap):CAS指令需要有3個操作數,分別是內存地址(在java中理解為變量的內存地址,用V表示)、舊的預期值(用A表示)和新值(用B表示)。CAS指令執行時,CAS指令指令時,當且僅當V處的值符合舊預期值A時,處理器用B更新V處的值,否則它就不執行更新,但是無論是否更新了V處的值,都會返回V的舊值,上述的處理過程是一個原子操作。 - 無需同步的方案
要保證線程安全,並不是一定就要進行同步,兩者沒有因果關系。同步只是保證共享數據爭用時的正確性的手段,如果一個方法本來就不涉及共享數據,那它自然就無需任何同步操作去保證正確性,因此會有一些代碼天生就是線程安全的。
1. 可重入代碼:這種代碼也叫做純代碼(pure code),可以在代碼執行的任何時刻中斷他,轉而去執行另外的代碼,而在控制權返回后,原來的程序不會發生任何錯誤。個人理解:這段代碼不會存儲任何的共享可變變量,只做處理邏輯。 2. 本地存儲(ThreadLocal) 如果一段代碼中所需的數據必須與其他代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行?如果能保證,我們就可以把共享數據的可見范圍限制在同一個線程之內。這樣無需同步也能保證線程之間不出現數據的爭用問題。 符合這種特點的應用並不少見,大部分使用消費隊列的架構模式(如“生產者-消費者”模式)都會將產品的消費過程盡量在一個線程中消費完。其中最重要的一個應用實例就是經