1. 程式人生 > >java多執行緒與併發程式設計詳解

java多執行緒與併發程式設計詳解

一、多執行緒

1、作業系統有兩個容易混淆的概念,程序和執行緒。

程序:一個計算機程式的執行例項,包含了需要執行的指令;有自己的獨立地址空間,包含程式內容和資料;不同程序的地址空間是互相隔離的;程序擁有各種資源和狀態資訊,包括開啟的檔案、子程序和訊號處理。

執行緒:表示程式的執行流程,是CPU排程執行的基本單位;執行緒有自己的程式計數器、暫存器、堆疊和幀。同一程序中的執行緒共用相同的地址空間,同時共享進程序鎖擁有的記憶體和其他資源。

2、Java標準庫提供了程序和執行緒相關的API,程序主要包括表示程序的java.lang.Process類和建立程序的java.lang.ProcessBuilder類;

表示執行緒的是java.lang.Thread類,在虛擬機器啟動之後,通常只有Java類的main方法這個普通執行緒執行,執行時可以建立和啟動新的執行緒;還有一類守護執行緒(damon thread),守護執行緒在後臺執行,提供程式執行時所需的服務。當虛擬機器中執行的所有執行緒都是守護執行緒時,虛擬機器終止執行。

3、執行緒間的可見性:一個執行緒對程序中共享的資料的修改,是否對另一個執行緒可見

可見性問題:

a、CPU採用時間片輪轉等不同演算法來對執行緒進行排程

  1. publicclass IdGenerator{  
  2.    privateint value = 0;  
  3.    publicint getNext(){  
  4.       return value++;  
  5.    }  
  6. }  
對於IdGenerator的getNext()方法,在多執行緒下不能保證返回值是不重複的:各個執行緒之間相互競爭CPU時間來獲取執行機會,CPU切換可能發生在執行間隙。

以上程式碼getNext()的指令序列:CPU切換可能發生在7條指令之間,多個getNext的指令交織在一起。

  1. aload_0  
  2. dup  
  3. getfield #12
  4. dup_x1  
  5. iconst_1  
  6. iadd  
  7. putfield #12
b、CPU快取:

目前CPU一般採用層次結構的多級快取的架構,有的CPU提供了L1、L2和L3三級快取。當CPU需要讀取主存中某個位置的資料時,會一次檢查各級快取中是否存在對應的資料。如果有,直接從快取中讀取,這比從主存中讀取速度快很多。當CPU需要寫入時,資料先被寫入快取中,之後再某個時間點寫回主存。所以某些時間點上,快取中的資料與主存中的資料可能是不一致。

c、指令順序重排

出行效能考慮,編譯器在編譯時可能會對位元組程式碼的指令順序進行重新排列,以優化指令的執行順序,在單執行緒中不會有問題,但在多執行緒可能產生與可見性相關的問題。

二、Java記憶體模型(Java Memory Model)

遮蔽了CPU快取等細節,只關注主存中的共享變數;關注物件的例項域、靜態域和陣列元素;關注執行緒間的動作。

1、volatile關鍵詞:用來對共享變數的訪問進行同步,上一次寫入操作的結果對下一次讀取操作是肯定可見的。(在寫入volatile變數值之後,CPU快取中的內容會被寫回記憶體;在讀取volatile變數時,CPU快取中的對應內容會被置為失效,重新從主存中進行讀取),volatile不使用鎖,效能優於synchronized關鍵詞。

用來確保對一個變數的修改被正確地傳播到其他執行緒中。

例子:A執行緒是Worker,一直跑迴圈,B執行緒呼叫setDone(true),A執行緒即停止任務

  1. publicclass Worker{  
  2.    privatevolatileboolean done;  
  3.    publicvoid setDone(boolean done){  
  4.       this.done = done;  
  5.    }  
  6.    publicvoid work(){  
  7.       while(!done){  
  8.          //執行任務;
  9.       }  
  10.    }  
  11. }  

例子:錯誤使用。因為沒有鎖的支援,volatile的修改不能依賴於當前值,當前值可能在其他執行緒中被修改。(Worker是直接賦新值與當前值無關)

  1. publicclass Counter {  
  2.     publicvolatilestaticint count = 0;  
  3.     publicstaticvoid inc() {  
  4.         //這裡延遲1毫秒,使得結果明顯
  5.         try {  
  6.             Thread.sleep(1);  
  7.         } catch (InterruptedException e) {  
  8.         }  
  9.         count++;  
  10.     }  
  11.     publicstaticvoid main(String[] args) {  
  12.         //同時啟動1000個執行緒,去進行i++計算,看看實際結果
  13.         for (int i = 0; i < 1000; i++) {  
  14.             new Thread(new Runnable() {  
  15.                 @Override
  16.                 publicvoid run() {  
  17.                     Counter.inc();  
  18.                 }  
  19.             }).start();  
  20.         }  
  21.         //這裡每次執行的值都有可能不同,可能不為1000
  22.         System.out.println("執行結果:Counter.count=" + Counter.count);  
  23.     }  
  24. }  
2、final關鍵詞
final關鍵詞宣告的域的值只能被初始化一次,一般在構造方法中初始化。。(在多執行緒開發中,final域通常用來實現不可變物件)

當物件中的共享變數的值不可能發生變化時,在多執行緒中也就不需要同步機制來進行處理,故在多執行緒開發中應儘可能使用不可變物件

另外,在程式碼執行時,final域的值可以被儲存在暫存器中,而不用從主存中頻繁重新讀取。

3、java基本型別的原子操作

1)基本型別,引用型別的複製引用是原子操作;(即一條指令完成)

2)long與double的賦值,引用是可以分割的,非原子操作;

3)要線上程間共享long或double的欄位時,必須在synchronized中操作,或是宣告成volatile

三、Java提供的執行緒同步方式

1、synchronized關鍵字

方法或程式碼塊的互斥性來完成實際上的一個原子操作。(方法或程式碼塊在被一個執行緒呼叫時,其他執行緒處於等待狀態)

所有的Java物件都有一個與synchronzied關聯的監視器物件(monitor),允許執行緒在該監視器物件上進行加鎖和解鎖操作。

a、靜態方法:Java類對應的Class類的物件所關聯的監視器物件。

b、例項方法:當前物件例項所關聯的監視器物件。

c、程式碼塊:程式碼塊宣告中的物件所關聯的監視器物件。

注:當鎖被釋放,對共享變數的修改會寫入主存;當活得鎖,CPU快取中的內容被置為無效。編譯器在處理synchronized方法或程式碼塊,不會把其中包含的程式碼移動到synchronized方法或程式碼塊之外,從而避免了由於程式碼重排而造成的問題。

例:以下方法getNext()和getNextV2() 都獲得了當前例項所關聯的監視器物件

  1. publicclass SynchronizedIdGenerator{  
  2.    privateint value = 0;  
  3.    publicsynchronizedint getNext(){  
  4.       return value++;  
  5.    }  
  6.    publicint getNextV2(){  
  7.       synchronized(this){  
  8.          return value++;  
  9.       }  
  10.    }  
  11. }  

2、Object類的wait、notify和notifyAll方法

生產者和消費者模式,判斷緩衝區是否滿來消費,緩衝區是否空來生產的邏輯。如果用while 和 volatile也可以做,不過本質上會讓執行緒處於忙等待,佔用CPU時間,對效能造成影響。

wait: 將當前執行緒放入,該物件的等待池中,執行緒A呼叫了B物件的wait()方法,執行緒A進入B物件的等待池,並且釋放B的鎖。(這裡,執行緒A必須持有B的鎖,所以呼叫的程式碼必須在synchronized修飾下,否則直接丟擲java.lang.IllegalMonitorStateException異常)。

notify:將該物件中等待池中的執行緒,隨機選取一個放入物件的鎖池,噹噹前執行緒結束後釋放掉鎖, 鎖池中的執行緒即可競爭物件的鎖來獲得執行機會。

notifyAll:將物件中等待池中的執行緒,全部放入鎖池。

(notify鎖喚醒的執行緒選擇由虛擬機器實現來決定,不能保證一個物件鎖關聯的等待集合中的執行緒按照所期望的順序被喚醒,很可能一個執行緒被喚醒之後,發現他所要求的條件並沒有滿足,而重新進入等待池。因為當等待池中包含多個執行緒時,一般使用notifyAll方法,不過該方法會導致執行緒在沒有必要的情況下被喚醒,之後又馬上進入等待池,對效能有影響,不過能保證程式的正確性)

工作流程:

a、Consumer執行緒A 來 看產品,發現產品為空,呼叫產品物件的wait(),執行緒A進入產品物件的等待池並釋放產品的鎖。

b、Producer執行緒B獲得產品的鎖,執行產品的notifyAll(),Consumer執行緒A從產品的等待池進入鎖池,Producer執行緒B生產產品,然後退出釋放鎖。

c、Consumer執行緒A獲得產品鎖,進入執行,發現有產品,消費產品,然後退出。

例子:

  1. publicsynchronized String pop(){  
  2.   this.notifyAll();// 喚醒物件等待池中的所有執行緒,可能喚醒的就是 生產者(當生產者發現產品滿,就會進入物件的等待池,這裡程式碼省略,基本略同)
  3.    while(index == -1){//如果發現沒產品,就釋放鎖,進入物件等待池
  4.       this.wait();  
  5.    }//當生產者生產完後,消費者從this.wait()方法再開始執行,第一次還會執行迴圈,萬一產品還是為空,則再等待,所以這裡必須用while迴圈,不能用if
  6.    String good = buffer[index];  
  7.    buffer[index] = null;  
  8.    index--;  
  9.    return good;// 消費完產品,退出。
  10. }  

注:wait()方法有超時和不超時之分,超時的在經過一段時間,執行緒還在物件的等待池中,那麼執行緒也會推出等待狀態。

3、執行緒狀態轉換:

已經廢棄的方法:stop、suspend、resume、destroy,這些方法在實現上時不安全的。

執行緒的狀態:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING(有超時的等待)、TERMINATED。

a、方法sleep()進入的阻塞狀態,不會釋放物件的鎖(即大家一起睡,誰也別想執行程式碼),所以不要讓sleep方法處在synchronized方法或程式碼塊中,否則造成其他等待獲取鎖的執行緒長時間處於等待。

b、方法join()則是主執行緒等待子執行緒完成,再往下執行。例如main方法新建兩個執行緒A和B

  1. publicstaticvoid main(String[] args) throws InterruptedException {    
  2. Thread t1 = new Thread(new ThreadTesterA());    
  3. Thread t2 = new Thread(new ThreadTesterB());    
  4. t1.start();    
  5. t1.join(); // 等t1執行完再往下執行
  6. t2.start();    
  7. t2.join(); // 在虛擬機器執行中,這句可能被忽略
  8. }  

c、方法interrupt(),向被呼叫的物件執行緒發起中斷請求。如執行緒A通過呼叫執行緒B的d的interrupt方法來發出中斷請求,執行緒B來處理這個請求,當然也可以忽略,這不是必須的。Object類的wait()、Thread類的join()和sleep方法都會丟擲受檢異常java.lang.InterruptedException,通過interrupt方法中斷該執行緒會導致執行緒離開等待狀態。對於wait()呼叫來說,執行緒需要重新獲取監視器物件上的鎖之後才能丟擲InterruptedException異常,並致以異常的處理邏輯。

可以通過Thread類的isInterrupted方法來判斷是否有中斷請求發生,通常可以利用這個方法來判斷是否退出執行緒(類似上面的volatitle修飾符的例子);

Thread類還有個方法Interrupted(),該方法不但可以判斷當前執行緒是否被中斷,還會清楚執行緒內部的中斷標記,如果返回true,即曾被請求中斷,同時呼叫完後,清除中斷標記。

如果一個執行緒在某個物件的等待池,那麼notify和interrupt 都可以使該執行緒從等待池中被移除。如果同時發生,那麼看實際發生順序。如果是notify先,那照常喚醒,沒影響。如果是interrupt先,並且虛擬機器選擇讓該執行緒中斷,那麼即使nofity,也會忽略該執行緒,而喚醒等待池中的另一個執行緒。

e、yield(),嘗試讓出所佔有的CPU資源,讓其他執行緒獲取執行機會,對作業系統上的排程器來說是一個訊號,不一定立即切換執行緒。(在實際開發中,測試階段頻繁呼叫yeid方法使執行緒切換更頻繁,從而讓一些多執行緒相關的錯誤更容易暴露出來)。


四、非阻塞方式

執行緒之間同步機制的核心是監視物件上的鎖,競爭鎖來獲得執行程式碼的機會。當一個物件獲取物件的鎖,然後其他嘗試獲取鎖的物件會處於等待狀態,這種鎖機制的實現方式很大程度限制了多執行緒程式的吞吐量和效能(執行緒阻塞),且會帶來死鎖(執行緒A有a物件鎖,等著獲取b物件鎖,執行緒B有b物件鎖,等待獲取a物件鎖)和優先順序倒置(優先順序低的執行緒獲得鎖,優先順序高的只能等待對方釋放鎖)等問題。

如果能不阻塞執行緒,又能保證多執行緒程式的正確性,就能有更好的效能。

在程式中,對共享變數的使用一般遵循一定的模式,即讀取、修改和寫入三步組成。之前碰到的問題是,這三步執行中可能執行緒執行切換,造成非原子操作。鎖機制是把這三步變成一個原子操作。

目前CPU本身實現 將這三步 合起來 形成一個原子操作,無需執行緒鎖機制干預,常見的指令是“比較和替換”(compare and swap,CAS),這個指令會先比較某個記憶體地址的當前值是不是指定的舊指,如果是,就用新值替換,否則什麼也不做,指令返回的結果是記憶體地址的當前值。通過CAS指令可以實現不依賴鎖機制的非阻塞演算法。一般做法是把CAS指令的呼叫放在一個無限迴圈中,不斷嘗試,知道CAS指令成功完成修改。

java.util.concurrent.atomic包中提供了CAS指令。(不是所有CPU都支援CAS,在某些平臺,java.util.concurrent.atomic的實現仍然是鎖機制)

atomic包中提供的Java類分成三類:

1、支援以原子操作來進行更新的資料型別的Java類(AtomicBoolean、AtomicInteger、AtomicReference),在記憶體模型相關的語義上,這四個類的物件類似於volatile變數。

相關推薦

java執行併發程式設計

一、多執行緒1、作業系統有兩個容易混淆的概念,程序和執行緒。程序:一個計算機程式的執行例項,包含了需要執行的指令;有自己的獨立地址空間,包含程式內容和資料;不同程序的地址空間是互相隔離的;程序擁有各種資源和狀態資訊,包括開啟的檔案、子程序和訊號處理。執行緒:表示程式的執行流程

執行併發程式設計

前言 多執行緒併發程式設計是Java程式設計中重要的一塊內容,也是面試重點覆蓋區域,所以學好多執行緒併發程式設計對我們來說極其重要,下面跟我一起開啟本次的學習之旅吧。 正文 執行緒與程序 1 執行緒:程序中負責程式執行的執行單元執行緒本身依靠程

免費初級中級高階大資料java視訊教程下載 加(微***信((號keepper,請備註java或掃下面2二3維4碼Java執行併發庫高階應用視訊教程下載

更多免費初級中級高階大資料java視訊教程下載 加(微***信((號keepper,請備註java或掃下面2二3維4碼Java多執行緒與併發庫高階應用視訊教程下載java視訊教程01_傳智播客_張孝祥_傳統執行緒技術回顧.rarjava視訊教程02_傳智播客_張孝祥_傳統定時器技術回顧.rarjava視訊教程

Java執行併發(三)

Condition等待和喚醒 在我們的並行程式中,避免不了某些寫成要預先規定好的順序執行,例如:先新增後修改,先買後賣,先進後出,對於這些場景,使用JUC的Conditon物件再合適不過了。 JUC中提供了Condition物件,用於讓指定執行緒等待與喚

Java執行併發(二)

Synchronized執行緒同步機制 很多執行緒同時對同一個資料或者檔案進行訪問的時候,對於這個檔案如果進行併發讀寫可能會產生問題。 多執行緒機制來保證同一個時間,只有一個執行緒對這個資源進行讀寫,來保證多執行緒環境下是健壯的。 程式碼案例:

Java執行併發(一)

多執行緒與併發的基礎問題 併發就是指程式同時處理多個任務的能力(一個程式被多個使用者訪問都能看到自己預期的結果) 併發的根源在於對多工情況下訪問資源的有效控制! 併發背後的問題 public class DownloadSimple {

Java執行併發庫高階應用之倒計時計數器CountDownLatch

CountDownLatch類是一個倒計時計數器,在完成一組正在其他執行緒中執行的操作之前,它允許一個或多個執行緒一直等待。用給定的計數初始化 CountDownLatch。由於呼叫了countDown() 方法,所以在當前計數到達零之前,await 方法會一直受阻塞。之後,

深入理解執行併發程式設計

一、多執行緒三大特性 1、原子性:一個操作或者多個操作要麼全部執行,要麼都不執行。 2、可見性:當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看到修改的值。 3、有序性:程式執行的順序是按照程式碼的先後順序執行的,在單執行緒

Java執行網路程式設計綜合使用

最近重新看多執行緒與網路程式設計這一塊的知識,好久沒碰這一塊了,都忘得差不多了,這裡將這兩個模組的知識串接一下。同時處理多執行緒與網路程式設計最為經典的例子莫過於聊天室,那我就聊天室案例作為一個回顧。 首先,我們來看以下程式碼: package MultiT

Java執行併發庫】3.傳統執行互斥技術

執行緒的同步互斥與通訊 互斥的問題在使用執行緒的時候是我們必須要注意的。 例如兩個執行緒同時開啟,由於業務規則,需要訪問同一個物件,要取得該物件 中的資料進行修改。 這樣多個執行緒對同一個資料進行操作的例項有很多,例如銀行交易。我們的賬戶中原來 有2000元,在同一時間,我

Java執行併發應用-(6)-執行之間共享物件和資料的方式

此內容來自張孝祥老師的java多執行緒與併發庫高階應用 如果多個執行緒執行的程式碼相同,可以使用同一個Runnable物件,這個Runnable物件中有那個共享資料。 如果多個執行緒執行的程式碼不同,這時候需要用不同的Runnable物件。將共享物件封裝在另一個物件中,然後

JAVA執行併發學習總結

一、 什麼是併發 在作業系統中,是指一個時間段中有幾個程式都處於已啟動執行到執行完畢之間,且這幾個程式都是在同一個處理機上執行,但任一個時刻點上只有一個程式在處理機上執行。 這裡需要注意併發和並行是不同的兩個概念。併發是指一個時間段內同時執行,這是個區間;而並行是指在同

java執行併發之建立執行的幾種方式

1、繼承Thread類方法 public class Demo1 extends Thread{ @Override public void run() { //判斷標誌 while(true) { System.out.println(get

Java 執行併發(六):AQS

我們前面幾張提到過,JUC 這個包裡面的工具類的底層就是使用 CAS 和 volatile 來保證執行緒安全的,整個 JUC 包裡面的類都是基於它們構建的。今天我們介紹一個非常重要的同步器,這個類是 JDK 在 CAS 和 volatile 的基礎上為我們提供的一個同步工具類。 背景 AbstractQueu

Java執行併發基礎面試題

> CS-LogN思維導圖:記錄專業基礎 面試題 開源地址:https://github.com/FISHers6/CS-LogN ![](https://img2020.cnblogs.com/blog/1454456/202006/1454456-20200619205756557-112814415

面試必備——Java執行併發(一)

1.程序和執行緒的 (1)由來 1)序列 最初的計算機只能接受一些特定的指令,使用者輸入一個指令,計算機就做出一個操作。當用戶在思考或者輸入時,計算機就在等待。顯然這樣效率低下,在很多時候,計算機都處在等待狀態。 2)批處理 提高計算機的效率,不用等待使用者的輸入,把一系列需要操作的指令寫下來,形成一個清單

Java執行Condition介面原理

Condition介面提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但是這兩者在使用方式以及功能特性上還是有差別的 Condition介面詳解 Condition定義了等待/通知兩種型別的方法,當前執行緒呼叫這些方法時,需要提前獲

Java執行同步和非同步

1. 多執行緒併發時,多個執行緒同時請求同一資源,必然導致此資源的資料不安全。 2. 執行緒池 在WEB服務中,對於web伺服器的響應速度必須儘可能的快,這就容不得在使用者提交請求按鈕後,再建立執行緒提供服務。為了減少使用者的等待時間,執行緒必須預先建立,放線上程池中,執行

JAVA執行Thread VS Runnable

要求 必備知識 本文要求基本瞭解JAVA程式設計知識。 開發環境 windows 7/EditPlus 演示地址 原始檔 程序與執行緒 程序是程式在處理機中的一次執行。一個程序既包括其所要執行的指令,也包括了執行指令所需的系統資源,不同程序所

Java 執行之synchronized關鍵字

package com.example; /** * Created by 晁東洋 on 2017/5/27. */ public class MyThreadClass { public static void main(String args[]){ Exampletest