Java進階07-線程安全

線程安全

定義

1.什麼是線程安全

當多個線程 同時去操作 共享資源時 能夠得到正確的結果就是線程安全。

2.爲什麼會有線程安全問題。

由於計算機的CPU運算能力比起和內存的交互能力高几個數量級,爲了不浪費CPU的運算能力,所以在主內存和CPU之間增加了一層高速緩存。

在這裏插入圖片描述

在這裏插入圖片描述

每次計算前,先從主存中讀取數據到高速緩存中,之後的計算就是通過高速緩存。等到最終計算完成後,再通過協議把高速緩存中的結果同步回主內存中。

同樣的Java也有直接的內存模型jmm

在這裏插入圖片描述
在這裏插入圖片描述

其中工作內存是私有的,而主內存是公有的。

比如:現在主內存中有個變量 sss=5;

線程A 把 sss拷貝到 工作內存,進行加 1,
線程B 也把 sss拷貝到自己的工作內存中,進行加1,

然後線程A把結果同步回主內存,此時sss=6;
線程B也把結果同步回主內存,sss=6.

這裏就出問題了,SSS 加了2次1,預期結果應該是7,但是實際卻是6 就出問題了。

在這裏插入圖片描述

在這裏插入圖片描述

關於主內存與工作內存之間具體的交互協議,即一個變量如何從主內存拷貝到工作內 存、如何從工作內存同步回主內存之類的實現細節,規定了8種操作,虛擬機實現時必須保證下面提及的每一種操作都是原子的、不可再分的:

  1. lock(鎖定):作用於主內存的變量,把一個變量標識爲一條線程獨佔狀態
  2. unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來 , 釋放後的變量纔可以被其他線程鎖定unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來 , 釋放後的變量纔可以被其他線程鎖定
  3. read(讀取) : 作用於主內存的變量 , 把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用read(讀取) : 作用於主內存的變量 , 把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
  4. load(載入):作用域工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中load(載入):作用域工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中
  5. use (使用) : 作用於工作內存的變量 , 把工作內存中的一個變量值傳遞給執行引擎use (使用) : 作用於工作內存的變量 , 把工作內存中的一個變量值傳遞給執行引擎
  6. assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦值給工作內存的變量
  7. store (存儲) : 作用於工作內存的變量 , 把工作內存中的一個變量的值傳送到主內存中 , 以便隨後的write的操作
  8. write (寫入) : 作用於主內存的變量, 它把store操作從工作內存中一個變量的值傳送到主內存的變量中

當然還有一些原則:

  1. 如果要把一個變量從主內存中複製到工作內存, 就需要按順序地執 行read和load操作 ,如果把變量從工作內存中同步回主內存中, 就要按順序地執行store和write操作. 但Java內存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行
  2. 不允許read和load、 store和write操作之一單獨出現
  3. 不允許一個線程丟棄它的最近assign的操作,即變量在工作
    內存中改變了之後必須同步到主內存中
  4. 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從工作內存同步回主內存中
  5. 一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量。 即就是對一個變量實施use和store操作之前 , 必須先執行過了assign和load操作
  6. 一個變量在同一時刻只允許一條線程對 其進行lock操作 , 但lock操作可以被同一條線程重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖。lock和unlock必須成對出現
  7. 如果對一個變量執行lock操作,將會清空工作內存中此變量的值, 在執行引擎使用這個變量前需要重新執行load或assign操作初始化變量的值
  8. 如果一個變量事先沒有被lock操作鎖定 , 則不允許 對它執行unlock操作 ; 也不允許去unlock一個被其他線程鎖定的變量
  9. 對一個變量執行unlock操作之前 , 必須先把此變量同步到主內存中(執行store和write操作)

3.怎麼保證線程安全

其實只要滿足3個條件就可以做到線程安全:

  1. 可見性:可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
  2. 原子性:即一個操作或者多個操作,要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
  3. 有序性:即程序執行的順序按照代碼的先後順序執行。

所以結合上面3個特性和java內存模型,我們有以下辦法保證線程安全:

  1. 不共享數據
  2. 使用關鍵字
  3. 使用JUC幫助類
  4. 原子操作

使用

原始不安全程序:

 public static void main(String[] args) {
		for (int i = 0; i < 20; i++) {
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					for (int j = 0; j < 100; j++) {
						sb++;
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				}
			}).start();
		}
		println(TAG, sb);
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		println(TAG, sb);
	}

上面的程序中,循環開啓20個線程,每個線程對sb自加1。我們預期的結果是2000。 但是每次運行,實際值都會小於2000。這裏就發生了上面說的線程不安全問題。sb++這個動作不是原子操作。虛擬機實際執行性的是:

int temp=sb;

temp =temp+1;

sb=temp;

1.使用synchronized關鍵字

這種方式最簡單,要注意的是儘量縮小同步代碼塊的範圍。只加在 操作共享數據的地方就行了。比如:

在這裏插入圖片描述

只要同步sb++這個操作就行了。結果就是2000。

要注意:

  1. 修飾實例方法,作用於當前實例加鎖,進入同步代碼前要獲得當前實例的鎖

  2. 修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖

  3. 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。

2. 使用volatile 關鍵字

其實單純使用volatile關鍵字並不能保證 線程安全,因爲volatile只能保證可見性,並不能保證原子性,所以還是不能保證線程安全。所以這裏基本沒用,一直就只用在單例模式中,和只需要可見性,如停止無限循環的線程。

3 使用原子類AtomicXXX

這些類是在JUC下,1.5提供的類,讓我們可以更加簡單的寫出線程安全代碼。例如:

在這裏插入圖片描述

紅色框中的代碼就可以保證 自加是原子操作。從而保證了線程安全。

4 使用Lock類加鎖

Loock是個接口,常用的實現類是ReentrantLock 可重入鎖。

在這裏插入圖片描述

要記得釋放鎖,否則就會死鎖 程序異常。

5 使用阻塞隊列BlockingQueue

阻塞隊列 系統源碼中使用的比較多,比如線程池中
在這裏插入圖片描述

這個適合生產者消費者模型。我們用的比較多的是ArrayBlockingQueue和LinkedBlockingQueue。一個是數組實現,一個是鏈表實現。 例子:

  private static BlockingQueue<Integer> sBlockingQueue=new ArrayBlockingQueue<Integer>(10);
	public static void main(String[] args) {
		sBlockingQueue.add(22);
		sBlockingQueue.add(23);
		new Thread(new Runnable() {
			@Override
			public void run() {
				for (; ; ) {
					try {
						println("take", sBlockingQueue.take());
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				sBlockingQueue.add(12);
				sBlockingQueue.add(22);
			}
		}).start();

運行結果:
在這裏插入圖片描述
第一個線程從隊列中取出數據,如果隊列沒數據就會阻塞 直到有數據。add 和 polll 不會阻塞。put 和 take會阻塞。

6 使用CountDownLatch

是一個同步輔助工具,允許一個或多個線程等待,直到在其他線程中執行的一組操作完成。

舉個例子,有三個工人在爲老闆幹活,這個老闆有一個習慣,就是當三個工人把一天的活都幹完了的時候,他就來檢查所有工人所幹的活。記住這個條件:三個工人先全部幹完活,老闆才檢查。所以在這裏用Java代碼設計兩個類,Worker代表工人,Boss代表老闆,具體的代碼實現如下:

工人:

 public class Worker extends Thread{
	
	private CountDownLatch downLatch;  
    private String name;  
      
    public Worker(CountDownLatch downLatch, String name){  
        this.downLatch = downLatch;  
        this.name = name;  
    }  
      
    public void run() {  
        this.doWork();  
        try{  
            TimeUnit.SECONDS.sleep(new Random().nextInt(10));  
        }catch(InterruptedException ie){  
        }  
        System.out.println(this.name + "活幹完了!");  
        this.downLatch.countDown();  
          
    }  
      
    private void doWork(){  
        System.out.println(this.name + "正在幹活!");  
    }  
}

老闆:

public class Boss extends Thread{
	
	 private CountDownLatch downLatch;  
     
	    public Boss(CountDownLatch downLatch){  
	        this.downLatch = downLatch;  
	    }  
	      
	    public void run() {  
	        System.out.println("老闆正在等所有的工人幹完活......");  
	        try {  
	            this.downLatch.await();  
	        } catch (InterruptedException e) {  
	        }  
	        System.out.println("工人活都幹完了,老闆開始檢查了!");  
	    }  
}

測試:

 public static void main(String[] args) {
		ExecutorService executor = Executors.newCachedThreadPool();  
        Worker w1 = new Worker(sCountDownLatch,"張三");  
        Worker w2 = new Worker(sCountDownLatch,"李四");  
        Worker w3 = new Worker(sCountDownLatch,"王二");  
        Boss boss = new Boss(sCountDownLatch);  
        executor.execute(w3);  
        executor.execute(w2);  
        executor.execute(w1);  
        executor.execute(boss);  
        executor.shutdown();  

在這裏插入圖片描述

怎麼運行都是 老闆等 3個工人幹完活纔去檢查。

7 使用ThreadLocal

使用這個類是無法保證線程同步的的,因爲這個類會爲每個線程創建一份數據,所以就不存在共享數據的問題了。

其實這個類應該是沒什麼用,並不能達到目的。

8 使用CyclicBarrier

這個和前面的CountDownLatch類似

對於CyclicBarrier,假設有一家公司要全體員工進行團建活動,活動內容爲翻越三個障礙物,每一個人翻越障礙物所用的時間是不一樣的。但是公司要求所有人在翻越當前障礙物之後再開始翻越下一個障礙物,也就是所有人翻越第一個障礙物之後,纔開始翻越第二個,以此類推。類比地,每一個員工都是一個“其他線程”。當所有人都翻越的所有的障礙物之後,程序才結束。而主線程可能早就結束了,這裏我們不用管主線程。

 private static CyclicBarrier sCyclicBarrier=new CyclicBarrier(3);
	
	
	
	
	public static void main(String[] args) {
		
		for (int i = 0; i < sCyclicBarrier.getParties(); i++) {
			new Thread(new Thread05(), "隊友"+i).start();
		}
		
		static class Thread05 implements Runnable {
		@Override
		public void run() {
			for(int i = 0; i < 3; i++) {
                try {
                    Random rand = new Random();
                    int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;//產生1000到3000之間的隨機整數
                    Thread.sleep(randomNum);
                    System.out.println(Thread.currentThread().getName() + ", 通過了第"+i+"個障礙物, 使用了 "+((double)randomNum/1000)+"s");
                    sCyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
		}
	}

運行結果:

在這裏插入圖片描述

CountDownLatch和CyclicBarrier都有讓多個線程等待同步然後再開始下一步動作的意思,但是CountDownLatch的下一步的動作實施者是主線程,具有不可重複性;而CyclicBarrier的下一步動作實施者還是“其他線程”本身,具有往復多次實施動作的特點。

原理

上面說的所有其實都是 JUC下的東西,所以我們重點就是搞清楚,JUC下的東西,那麼線程安全就沒問題了。

JUC就是 1.5增加的 java.util.concurrent 包下的所有類。

大致可以分爲以下幾類:

鎖類

原子類

阻塞隊列類

併發輔助工具類

線程池類

等等。

不過它們的基礎是 CAS ,AQS,synchronize,volatile和各種鎖。基本結構如下:

在這裏插入圖片描述

1. synchronized的實現原理

原子性(Atomicity):由Java內存模型來直接保證的原子性變量操作包括read、load、 assign、use、store和write。
如果應用場景需要一個更大範圍的原子性保證(經常會遇到),Java內存模型還提供了 lock和unlock操作來滿足這種需求,儘管虛擬機未把lock和unlock操作直接開放給用戶使用, 但是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱式地使用這兩個操作,這 兩個字節碼指令反映到Java代碼中就是同步塊——synchronized關鍵字,因此在synchronized塊 之間的操作也具備原子性。

Java 虛擬機中的同步(Synchronization)基於進入和退出管程(Monitor)對象實現, 無論是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步代碼塊)還是隱式同步都是如此

在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例數據和對齊填充。如下:

在這裏插入圖片描述

實例變量:存放類的屬性數據信息,包括父類的屬性信息,如果是數組的實例部分還包括數組的長度,這部分內存按4字節對齊。

填充數據:由於虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊,這點了解即可。

對象頭:由Mark Word 和Class Metedata Address組成。
其中Mark Word在默認情況下存儲着對象的HashCode、分代年齡、鎖標記位等

默認的存儲結構爲:
在這裏插入圖片描述

它還會根據 不同的狀態 有不同的結構:

在這裏插入圖片描述

這裏我們主要分析一下重量級鎖也就是通常說synchronized的對象鎖,鎖標識位爲10,其中指針指向的是monitor對象(也稱爲管程或監視器鎖)的起始地址。每個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,如monitor可以與對象一起創建銷燬或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處於鎖定狀態。在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,其主要數據結構如下(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的)

在這裏插入圖片描述

ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSe t集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。如下圖所示

在這裏插入圖片描述

monitor對象存在於每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是爲什麼Java中任意對象可以作爲鎖的原因,同時也是notify/notifyAll/wait等方法存在於頂級對象Object中的原因

任何一個對象都有一個monitor與之關聯,當一個monitor被某個線程持有之後,該對象將處於鎖定狀態。同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程佔用。

synchronized修飾的方法並沒有monitorenter指令和monitorexit指令,取得代之的確實是ACC_SYNCHRONIZED標識,該標識指明瞭該方法是一個同步方法,JVM通過該ACC_SYNCHRONIZED訪問標誌來辨別一個方法是否聲明爲同步方法,從而執行相應的同步調用。這便是synchronized鎖在同步代碼塊和同步方法上實現的基本原理。同時我們還必須注意到的是在Java早期版本中,synchronized屬於重量級鎖,效率低下,因爲監視器鎖(monitor)是依賴於底層的操作系統的Mutex Lock來實現的,而操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是爲什麼早期的synchronized效率低的原因。Java 6之後,爲了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖。

synchronized的可重入性
從互斥鎖的設計上來說,當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,將會處於阻塞狀態,但當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬於重入鎖,請求將會成功,在java中synchronized是基於原子性的內部鎖機制,是可重入的,因此在一個線程調用synchronized方法的同時在其方法體內部調用該對象另一個synchronized方法,也就是說一個線程得到一個對象鎖後再次請求該對象鎖,是允許的,這就是synchronized的可重入性。

等待喚醒機制與synchronized
所謂等待喚醒機制本篇主要指的是notify/notifyAll和wait方法,在使用這3個方法時,必須處於synchronized代碼塊或者synchronized方法中,否則就會拋出IllegalMonitorStateException異常,這是因爲調用這幾個方法前必須拿到當前對象的監視器monitor對象,也就是說notify/notifyAll和wait方法依賴於monitor對象,在前面的分析中,我們知道monitor 存在於對象頭的Mark Word 中(存儲monitor引用指針),而synchronized關鍵字可以獲取 monitor ,這也就是爲什麼notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調用的原因。

synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}

2. volatile的實現原理

volatile的特性:

volatile可見性:對一個volatile的讀,總可以看到對這個變量最終的寫;

volatile原子性:volatile對單個讀/寫具有原子性(32位Long、Double),但是複合操作除外,例如:i++;

jvm底層採用“內存屏障”來實現volatile語義。

當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值立即刷新到主內存中;

當讀一個volatile變量時,JMM會把該線程對應的本地內存設置爲無效,直接從主內存中讀取共享變量。

java中volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改後可以立即同步到主內存,被其修飾的變量在每次使用之前都從主內存刷新。因此,可以使用volatile來保證多線程操作時變量的可見性。

觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令。這個指令就相當於一個內存屏障

3. CAS

CAS的全稱是Compare And Swap 即比較交換,其算法核心思想如下

執行函數:CAS(V,E,N)

其包含3個參數

V表示要更新的變量

E表示預期值

N表示新值

如果V值等於E值,則將V的值設爲N。若V值和E值不同,則說明已經有其他線程做了更新,則當前線程什麼都不做。通俗的理解就是CAS操作需要我們提供一個期望值,當期望值與當前線程的變量值相同時,說明還沒線程修改該值,當前線程可以進行修改,也就是執行CAS操作,但如果期望值與當前線程不符,則說明該值已被其他線程修改,此時不執行更新操作,但可以選擇重新讀取該變量再嘗試再次修改該變量,也可以放棄操作

在這裏插入圖片描述

由於CAS操作屬於樂觀派,它總認爲自己可以成功完成操作,當多個線程同時使用CAS操作一個變量時,只有一個會勝出,併成功更新,其餘均會失敗,但失敗的線程並不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的線程放棄操作,這點從圖中也可以看出來。基於這樣的原理,CAS操作即使沒有鎖,同樣知道其他線程對共享資源操作影響,並執行相應的處理措施。同時從這點也可以看出,由於無鎖操作中沒有鎖的存在,因此不可能出現死鎖的情況,也就是說無鎖操作天生免疫死鎖。

CPU指令對CAS的支持
或許我們可能會有這樣的疑問,假設存在多個線程執行CAS操作並且CAS的步驟很多,有沒有可能在判斷V和E相同後,正要賦值時,切換了線程,更改了值。造成了數據不一致呢?答案是否定的,因爲CAS是一種系統原語,原語屬於操作系統用語範疇,是由若干條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的數據不一致問題。

compareAndSet這個方法主要調用unsafe.compareAndSwapInt這個方法,這個方法有四個參數,其中第一個參數爲需要改變的對象,第二個爲偏移量(即之前求出來的valueOffset的值),第三個參數爲期待的值,第四個爲更新後的值。整個方法的作用即爲若調用該方法時,value的值與expect這個值相等,那麼則將value修改爲update這個值,並返回一個true,如果調用該方法時,value的值與expect這個值不相等,那麼不做任何操作,並範圍一個false。

鮮爲人知的指針: Unsafe類

在這裏插入圖片描述

關於Unsafe類的主要功能點如下

在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述

4. AtomicInteger原理

這個類提供int型的原子操作,使用上面已經說的很清楚,這裏我們主要看看 它的 原子子操作怎麼實現的。

在這裏插入圖片描述

這裏的內存中的偏移量就是字段在 對象中的內存地址。

先看構造方法:

在這裏插入圖片描述

沒什麼東西,裏面有個一個int變量,被volatile修飾。

接着看下它的compareAndSet方法:

在這裏插入圖片描述

只有一句就是調用unsafe的compareAndSwapInt。

在這裏插入圖片描述

compareAndSet這個方法主要調用unsafe.compareAndSwapInt這個方法,這個方法有四個參數,其中第一個參數爲需要改變的對象,第二個爲偏移量(即之前求出來的valueOffset的值),第三個參數爲期待的值,第四個爲更新後的值。整個方法的作用即爲若調用該方法時,value的值與expect這個值相等,那麼則將value修改爲update這個值,並返回一個true,如果調用該方法時,value的值與expect這個值不相等,那麼不做任何操作,並範圍一個false。

這個方法是原生方法。

基本沒有了,可以看到其實 AtomicInteger類 還是很簡單的 主要就是 調用了 unsafe提供 原子操作方法。

5. AQS

AbstractQueuedSynchronizer又稱爲隊列同步器(後面簡稱AQS),它是用來構建鎖或其他同步組件的基礎框架,內部通過一個int類型的成員變量state來控制同步狀態,當state=0時,則說明沒有任何線程佔有共享資源的鎖,當state=1時,則說明有線程目前正在使用共享變量,其他線程必須加入同步隊列進行等待,AQS內部通過內部類Node構成FIFO的同步隊列來完成線程獲取鎖的排隊工作,同時利用內部類ConditionObject構建等待隊列,當Condition調用wait()方法後,線程將會加入等待隊列中,而當Condition調用signal()方法後,線程將從等待隊列轉移動同步隊列中進行鎖競爭。注意這裏涉及到兩種隊列,一種的同步隊列,當線程請求鎖而等待的後將加入同步隊列等待,而另一種則是等待隊列(可有多個),通過Condition調用await()方法釋放鎖後,將加入等待隊列。

在這裏插入圖片描述

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mwQWc6lv-1578466279456)(84E77DE7C897430CBCBD031C0B4615C4)]

head和tail分別是AQS中的變量,其中head指向同步隊列的頭部,注意head爲空結點,不存儲信息。而tail則是同步隊列的隊尾,同步隊列採用的是雙向鏈表的結構這樣可方便隊列進行結點增刪操作。state變量則是代表同步狀態,執行當線程調用lock方法進行加鎖後,如果此時state的值爲0

則說明當前線程可以獲取到鎖,同時將state設置爲1,表示獲取成功。如果state已爲1,也就是當前鎖已被其他線程持有,那麼當前執行線程將被封裝爲Node結點加入同步隊列等待。其中Node結點是對每一個訪問同步代碼的線程的封裝,從圖中的Node的數據結構也可看出,其包含了需要同步的線程本身以及線程的狀態,如是否被阻塞,是否等待喚醒,是否已經被取消等。每個Node結點內部關聯其前繼結點prev和後繼結點next,這樣可以方便線程釋放鎖後快速喚醒下一個在等待的線程,

在這裏插入圖片描述

其中SHARED和EXCLUSIVE常量分別代表共享模式和獨佔模式,所謂共享模式是一個鎖允許多條線程同時操作,如信號量Semaphore採用的就是基於AQS的共享模式實現的,而獨佔模式則是同一個時間段只能有一個線程對共享資源進行操作,多餘的請求線程需要排隊等待,如ReentranLock。變量waitStatus則表示當前被封裝成Node結點的等待狀態,共有4種取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。

6. ReentrantLock原理

內部使用了AQS完成,先看Lock方法。

在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

總體流程就是 如果獲取鎖狀態成功,則把當前的線程設置進去,如果失敗則 包裝爲一個node添加到隊尾。添加到同步隊列後,結點就會進入一個自旋過程,即每個結點都在觀察時機待條件滿足獲取同步狀態,然後從同步隊列退出並結束自旋,回到之前的acquire()方法,自旋過程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中執行的,代碼如下

在這裏插入圖片描述

頭結點是擁有鎖的,正在運行的那一個線程。

7. Condition原理

Condition是接口,有一下方法:
在這裏插入圖片描述

它的實現類在AbstractQueuedSynchronizer中的 ConditionObject。

在這裏插入圖片描述

7. Semaphore原理

信號量(Semaphore),又被稱爲信號燈,在多線程環境下用於協調各個線程, 以保證它們能夠正確、合理的使用公共資源。信號量維護了一個許可集,我們在初始化Semaphore時需要爲這個許可集傳入一個數量值,該數量值代表同一時間能訪問共享資源的線程數量。線程可以通過acquire()方法獲取到一個許可,然後對共享資源進行操作,注意如果許可集已分配完了,那麼線程將進入等待狀態,直到其他線程釋放許可纔有機會再獲取許可,線程釋放一個許可通過release()方法完成。

8. 阻塞隊列BolckingQueue原理

阻塞隊列與我們平常接觸的普通隊列(LinkedList或ArrayList等)的最大不同點,在於阻塞隊列支出阻塞添加和阻塞刪除方法。

阻塞添加
所謂的阻塞添加是指當阻塞隊列元素已滿時,隊列會阻塞加入元素的線程,直隊列元素不滿時才重新喚醒線程執行元素加入操作。

阻塞刪除
阻塞刪除是指在隊列元素爲空時,刪除隊列元素的線程將被阻塞,直到隊列不爲空再執行刪除操作(一般都會返回被刪除的元素)

阻塞隊列的主要實現類爲:LinkedBlockingQueue與ArrayBlockingQueue

我們先看BlockingQueque接口有哪些方法:

插入方法:

add(E e) : 添加成功返回true,失敗拋IllegalStateException異常
offer(E e) : 成功返回 true,如果此隊列已滿,則返回 false。
put(E e) :將元素插入此隊列的尾部,如果該隊列已滿,則一直阻塞

刪除方法:

remove(Object o) :移除指定元素,成功返回true,失敗返回false
poll() : 獲取並移除此隊列的頭元素,若隊列爲空,則返回 null
take():獲取並移除此隊列頭元素,若沒有元素則一直阻塞。

檢查方法

element() :獲取但不移除此隊列的頭元素,沒有元素則拋異常
peek() :獲取但不移除此隊列的頭;若隊列爲空,則返回 null。

ArrayBlockingQueue的內部是通過一個可重入鎖ReentrantLock和兩個Condition條件對象來實現阻塞:

在這裏插入圖片描述

從成員變量可看出,ArrayBlockingQueue內部確實是通過數組對象items來存儲所有的數據,值得注意的是ArrayBlockingQueue通過一個ReentrantLock來同時控制添加線程與移除線程的並非訪問,這點與LinkedBlockingQueue區別很大(稍後會分析)。而對於notEmpty條件對象則是用於存放等待或喚醒調用take方法的線程,告訴他們隊列已有元素,可以執行獲取操作。同理notFull條件對象是用於等待或喚醒調用put方法的線程,告訴它們,隊列未滿,可以執行添加元素的操作。takeIndex代表的是下一個方法(take,poll,peek,remove)被調用時獲取數組元素的索引,putIndex則代表下一個方法(put, offer, or add)被調用時元素添加到數組中的索引。圖示如下

在這裏插入圖片描述

首先看 put 阻塞添加方法:

在這裏插入圖片描述

很簡單如果當前隊列已滿就等待。

在這裏插入圖片描述

否則把自己添加進隊列,然後通知喚醒。

put方法是一個阻塞的方法,如果隊列元素已滿,那麼當前線程將會被notFull條件對象掛起加到等待隊列中,直到隊列有空檔纔會喚醒執行添加操作。但如果隊列沒有滿,那麼就直接調用enqueue(e)方法將元素加入到數組隊列中。到此我們對三個添加方法即put,offer,add都分析完畢,其中offer,add在正常情況下都是無阻塞的添加,而put方法是阻塞添加。這就是阻塞隊列的添加過程。說白了就是當隊列滿時通過條件對象Condtion來阻塞當前調用put方法的線程,直到線程又再次被喚醒執行。總得來說添加線程的執行存在以下兩種情況,一是,隊列已滿,那麼新到來的put線程將添加到notFull的條件隊列中等待,二是,有移除線程執行移除操作,移除成功同時喚醒put線程,如下圖所示

在這裏插入圖片描述

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-XQjB88XG-1578466279468)(F3BE9105D20A4699A9C8FFF9D1B44920)]

接下來看看 take 和 poll ;

在這裏插入圖片描述

區別就是take 會 阻塞等待。

通過上述的分析,對於LinkedBlockingQueue和ArrayBlockingQueue的基本使用以及內部實現原理我們已較爲熟悉了,這裏我們就對它們兩間的區別來個小結

1.隊列大小有所不同,ArrayBlockingQueue是有界的初始化必須指定大小,而LinkedBlockingQueue可以是有界的也可以是無界的(Integer.MAX_VALUE),對於後者而言,當添加速度大於移除速度時,在無界的情況下,可能會造成內存溢出等問題。

2.數據存儲容器不同,ArrayBlockingQueue採用的是數組作爲數據存儲容器,而LinkedBlockingQueue採用的則是以Node節點作爲連接對象的鏈表。

3.由於ArrayBlockingQueue採用的是數組的存儲容器,因此在插入或刪除元素時不會產生或銷燬任何額外的對象實例,而LinkedBlockingQueue則會生成一個額外的Node對象。這可能在長時間內需要高效併發地處理大批量數據的時,對於GC可能存在較大影響。

4.兩者的實現隊列添加或移除的鎖不一樣,ArrayBlockingQueue實現的隊列中的鎖是沒有分離的,即添加操作和移除操作採用的同一個ReenterLock鎖,而LinkedBlockingQueue實現的隊列中的鎖是分離的,其添加採用的是putLock,移除採用的則是takeLock,這樣能大大提高隊列的吞吐量,也意味着在高併發的情況下生產者和消費者可以並行地操作隊列中的數據,以此來提高整個隊列的併發性能。

總結

線程安全就是共享資源同一時刻只能右一個線程去讀寫

深入剖析基於併發AQS

這個連接還需要認真看看。

同步的內容有點多

先發放一放,慢慢消化後 再來完善

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章