一、併發的基本概念:同時擁有兩個或者多個線程,若是程序在單核處理器上運行,多個線程將交替地換入或者換出內存,這些線程是同時存在的,每一個線程都處於執行過程當中的某個狀態。若是容許在多核處理器上,此時程序中的每一個線程都將分配到一個處理器核上,所以能夠同時運行。併發,多個線程操做相同的資源,保證線程安全,合理利用資源。html
二、高併發的概念:高併發(High Concurrency)是互聯網分佈式系統架構設計中必須考慮的因素之一,它一般是指,經過設計保證系統可以同時並行處理不少請求。
高併發,服務能同時處理不少請求,提升程序性能。主要是指系統運行過程當中,短期內,遇到大量操做請求的狀況,主要發生在系統集中收到大量請求,好比12306的搶票,天貓雙十一的活動,這種狀況的發生就會致使系統在這段時間內執行大量的操做,例如對資源的請求,數據庫的操做等等。java
三、併發編程與線程安全:線程安全就是代碼所在的進程有多個線程在同時執行,而這些線程k可能會運行同一段代碼,若是每次運行結果和單線程運行結果一致,並且其餘變量的值也和預期是同樣的,咱們就認爲這是線程安全的,就是併發環境下獲得咱們指望的正確的結果。線程不安全不提供數據訪問保護,有可能出現多個線程前後更改數據形成所獲得的數據出現髒數據,也可能在計算的時候出現錯誤。算法
四、併發模擬的幾種方式。數據庫
第一種:Postman,Http請求模擬工具。
第二種:Apache Bench(簡稱AB),Apache附帶的工具,測試網站性能。
第三種:JMeter,Apache組織開發的壓力測試工具。
第四種:Semaphore、CountDownLatch等等代碼進行併發模擬測試。
apache
4.一、Postman,Http請求模擬工具,測試以下所示:編程
點擊Run之後,設置完畢參數開始執行。數組
執行完的效果以下所示:緩存
4.二、Apache Bench(簡稱AB),Apache附帶的工具,測試網站性能。AB是一個命令行的工具,輸入命令就能夠進行測試,對發起負載的本機要求很低,根據ab命令能夠建立不少的併發訪問線程,模擬多個訪問者同時對同一個url地址進行訪問,所以能夠用來測試目標服務器的負載壓力。安全
AB指定命令發送請求之後,能夠獲得每秒產生的字節數、每次處理請求的時間、每秒處理請求的數目等等統計數據。bash
安裝Apache服務器,官網下載地址:https://www.apachelounge.com/download/
在D:\biehl\ApacheBench\Apache24\bin目錄下面找到ab.exe。-n是本次測試的總數,-c是指定本次併發數。
4.三、JMeter,Apache組織開發的壓力測試工具。官網地址:https://jmeter.apache.org/
在D:\biehl\JMeter\apache-jmeter-5.2.1\bin目錄下面執行jmeter.bat腳本文件。
4.四、Semaphore、CountDownLatch等等代碼進行併發模擬測試。
1 package com.bie.concurrency.test; 2 3 import java.util.concurrent.CountDownLatch; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 import java.util.concurrent.Semaphore; 7 8 import com.bie.concurrency.annoations.NotThreadSafe; 9 10 import lombok.extern.slf4j.Slf4j; 11 12 /** 13 * 14 * 15 * @Title: CountDownLatchTest.java 16 * @Package com.bie.concurrency.test 17 * @Description: TODO 18 * @author biehl 19 * @date 2020年1月2日 20 * @version V1.0 21 * 22 * 併發模擬測試的程序。 23 * 24 * 一、CountDownLatch計數器向下減的閉鎖類。該類能夠阻塞線程,並保證線程在知足某種特定的條件下繼續執行。 25 * CountDownLatch比較適合咱們保證線程執行完以後再繼續其餘的處理。 26 * 27 * 二、Semaphore信號量,實現的功能是能夠阻塞進程而且控制同一時間的請求的併發量。 28 * Semaphore更適合控制同時併發的線程數。 29 * 30 * 三、CountDownLatch、Semaphore配合線程池一塊兒使用。 31 * 32 */ 33 @Slf4j 34 @NotThreadSafe // 因爲每次結果不一致,因此是線程不安全的類。不要使用此程序進行併發測試。 35 public class ConcurrencyTest { 36 37 public static int clientTotal = 5000;// 1000個請求,請求總數 38 39 public static int threadTotal = 200;// 容許同時併發執行的線程數目 40 41 public static int count = 0;// 計數的值 42 43 // 自增計數器 44 private static void add() { 45 count++; 46 } 47 48 public static void main(String[] args) { 49 // 定義線程池 50 ExecutorService executorService = Executors.newCachedThreadPool(); 51 // 定義信號量,信號量裏面須要定義容許併發的數量 52 final Semaphore semaphore = new Semaphore(threadTotal); 53 // 定義計數器閉鎖,但願全部請求完之後統計計數結果,將計數結果放入 54 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); 55 // 放入請求操做 56 for (int i = 0; i < clientTotal; i++) { 57 // 全部請求放入到線程池結果中 58 executorService.execute(() -> { 59 // 在線程池執行的時候引入了信號量,信號量每次作acquire()操做的時候就是判斷當前進程是否容許被執行。 60 // 若是達到了必定併發數的時候,add方法可能會臨時被阻塞掉。當acquire()能夠返回值的時候,add方法能夠被執行。 61 // add方法執行完畢之後,釋放當前進程,此時信號量就已經引入完畢了。 62 // 在引入信號量的基礎上引入閉鎖機制。countDownLatch 63 try { 64 // 執行核心執行方法以前引入信號量,信號量每次容許執行以前須要調用方法acquire()。 65 semaphore.acquire(); 66 // 核心執行方法。 67 add(); 68 // 核心執行方法執行完成之後,須要釋放當前進程,釋放信號量。 69 semaphore.release(); 70 } catch (InterruptedException e) { 71 e.printStackTrace(); 72 } 73 // try-catch是一次執行系統的操做,執行完畢之後調用一下閉鎖。 74 // 每次執行完畢之後countDownLatch裏面對應的計算值減一。 75 // 執行countDown()方法計數器減一。 76 countDownLatch.countDown(); 77 }); 78 } 79 // 這個方法能夠保證以前的countDownLatch必須減爲0,減爲0的前提就是全部的進程必須執行完畢。 80 try { 81 // 調用await()方法當前進程進入等待狀態。 82 countDownLatch.await(); 83 } catch (InterruptedException e) { 84 e.printStackTrace(); 85 } 86 // 一般,線程池執行完畢之後,線程池再也不使用,記得關閉線程池 87 executorService.shutdown(); 88 // 若是咱們但願在全部線程執行完畢之後打印當前計數的值。只須要log.info以前執行上一步便可countDownLatch.await();。 89 log.info("count:{}", count); 90 91 } 92 93 }
五、線程安全性。
線程安全性的定義,當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些進程將如何交替執行,而且在主調代碼中不須要任何額外的同步或者協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。
六、線程安全性主要體如今三個方面原子性、可見性、有序性。
a、原子性,提供了互斥訪問,同一時刻只能有一個線程來對它進行操做。
b、可見性,一個線程對主內存的修改能夠及時的被其餘線程觀察到。
c、有序性,一個線程觀察其餘線程中的指令執行順序,因爲指令重排序的存在,該觀察結果通常雜亂無序。
七、線程安全性的原子性的底層代碼理解,以下所示。
1 public static AtomicInteger count = new AtomicInteger(0);// 計數的值,count的值是在工做內存中的,而var5就是主內存的值,能夠進行參考學習。 2 // 自增操做 3 count.incrementAndGet(); 4 5 // 調用AtomicInteger.incrementAndGet()方法。 6 public final int incrementAndGet() { 7 // this是調用的值,如上面定義的count變量 8 // 裏面的三個參數對象下面方法的var一、var二、var4 9 return unsafe.getAndAddInt(this, valueOffset, 1) + 1; 10 } 11 12 // 調用Unsafe的getAndAddInt();方法 13 // var1是傳遞的值,好比本身定義的count 14 // var2是當前的值,好比當前值是2 15 // var4是1,好比當前值是1 16 public final int getAndAddInt(Object var1, long var2, int var4) { 17 // 定義變量var5 18 int var5; 19 // do循環 20 do { 21 // var5是調用底層方法獲取到的值,調用底層方法獲得底層當前的值。 22 // 此時,若是沒有其餘線程處理var1的時候,正常返回的值應該是2。 23 var5 = this.getIntVolatile(var1, var2); 24 // 此時,傳遞到compareAndSwapInt的參數是count對象、var2是二、var5從底層傳遞的二、最後一個參數var5 + var4從底層傳遞的值加上1。 25 // 這個方法但願打到的目的是對於var1這個count對象,若是當前的var2的值和底層的這個var5的值一致,把它count更新成var5 + var4從底層傳遞的值加上1。 26 // 若是執行此處更新操做的時候,把它count更新成var5 + var4從底層傳遞的值加上1的時候可能被其餘線程修改,所以這裏判斷若是當前值var2和指望值var5相同的話,就容許var5 + var4這個加1操做的。 27 // 不然,從新取出var5,好比是3,而後var2從新從var1中取出,好比是3,再次進行判斷。此時var2等於var5,那麼此時最後一個參數var5 + var4等於4。 28 // 核心原理,當前對象var1的值var2,去和底層的var5的值進行對比,若是當前的值var2和底層的值var5相等,就執行var5+var4操做,不然就一直進行循環操做。 29 } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); 30 // 返回底層的值var5 31 return var5; 32 }
7.一、線程安全性的原子性的使用,以下所示:
atomic包裏面AtomicInteger類,調用了Unsafe類實現自增操做。this.compareAndSwapInt()方法核心就是CAS的核心。CAS實現的原理是拿當前的對象和底層裏面的值進行對比,若是當前對象的值和底層的值一致的時候才執行對應的加一操做。
1 package com.bie.concurrency.atomic; 2 3 import java.util.concurrent.CountDownLatch; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 import java.util.concurrent.Semaphore; 7 import java.util.concurrent.atomic.AtomicInteger; 8 9 import com.bie.concurrency.annoations.NotThreadSafe; 10 11 import lombok.extern.slf4j.Slf4j; 12 13 /** 14 * 15 * 16 * @Title: CountDownLatchTest.java 17 * @Package com.bie.concurrency.test 18 * @Description: TODO 19 * @author biehl 20 * @date 2020年1月2日 21 * @version V1.0 22 * 23 * 併發模擬測試的程序。 24 * 25 * 一、CountDownLatch計數器向下減的閉鎖類。該類能夠阻塞線程,並保證線程在知足某種特定的條件下繼續執行。 26 * CountDownLatch比較適合咱們保證線程執行完以後再繼續其餘的處理。 27 * 28 * 二、Semaphore信號量,實現的功能是能夠阻塞進程而且控制同一時間的請求的併發量。 Semaphore更適合控制同時併發的線程數。 29 * 30 * 三、CountDownLatch、Semaphore配合線程池一塊兒使用。 31 * 32 * 四、jdk提供了Atomic包,來實現原子性,Atomic包裏面提供了不少AtomicXXX類,他們都是經過CAS來完成原子性的。 33 * 34 * 五、atomic包裏面AtomicInteger類,調用了Unsafe類實現自增操做。 35 * unsafe.getAndAddInt(this, valueOffset, 1) + 1; 36 * this.compareAndSwapInt()方法核心就是CAS的核心。CAS實現的原理是拿當前的對象和底層裏面的值進行對比,若是當前對象的值和底層的值一致的時候才執行對應的加一操做。 37 * 38 */ 39 @Slf4j 40 @ThreadSafe // 因爲每次結果一致,因此是線程安全的類。可使用此程序進行併發測試。 41 public class ConcurrencyAtomicExample1 { 42 43 public static int clientTotal = 5000;// 5000個請求,請求總數 44 45 public static int threadTotal = 200;// 容許同時併發執行的線程數目 46 47 // int基本數據類型對應的atomic包裏面的類是AtomicInteger類型的。 48 // 初始化值爲0 49 public static AtomicInteger count = new AtomicInteger(0);// 計數的值 50 51 // 自增計數器 52 private static void add() { 53 // 自增操做調用的方法,類比++i 54 count.incrementAndGet(); 55 // 或者調用下面的方法,類比i++ 56 // count.getAndIncrement(); 57 } 58 59 public static void main(String[] args) { 60 // 定義線程池 61 ExecutorService executorService = Executors.newCachedThreadPool(); 62 // 定義信號量,信號量裏面須要定義容許併發的數量 63 final Semaphore semaphore = new Semaphore(threadTotal); 64 // 定義計數器閉鎖,但願全部請求完之後統計計數結果,將計數結果放入 65 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); 66 // 放入請求操做 67 for (int i = 0; i < clientTotal; i++) { 68 // 全部請求放入到線程池結果中 69 executorService.execute(() -> { 70 // 在線程池執行的時候引入了信號量,信號量每次作acquire()操做的時候就是判斷當前進程是否容許被執行。 71 // 若是達到了必定併發數的時候,add方法可能會臨時被阻塞掉。當acquire()能夠返回值的時候,add方法能夠被執行。 72 // add方法執行完畢之後,釋放當前進程,此時信號量就已經引入完畢了。 73 // 在引入信號量的基礎上引入閉鎖機制。countDownLatch 74 try { 75 // 執行核心執行方法以前引入信號量,信號量每次容許執行以前須要調用方法acquire()。 76 semaphore.acquire(); 77 // 核心執行方法。 78 add(); 79 // 核心執行方法執行完成之後,須要釋放當前進程,釋放信號量。 80 semaphore.release(); 81 } catch (InterruptedException e) { 82 e.printStackTrace(); 83 } 84 // try-catch是一次執行系統的操做,執行完畢之後調用一下閉鎖。 85 // 每次執行完畢之後countDownLatch裏面對應的計算值減一。 86 // 執行countDown()方法計數器減一。 87 countDownLatch.countDown(); 88 }); 89 } 90 // 這個方法能夠保證以前的countDownLatch必須減爲0,減爲0的前提就是全部的進程必須執行完畢。 91 try { 92 // 調用await()方法當前進程進入等待狀態。 93 countDownLatch.await(); 94 } catch (InterruptedException e) { 95 e.printStackTrace(); 96 } 97 // 一般,線程池執行完畢之後,線程池再也不使用,記得關閉線程池 98 executorService.shutdown(); 99 // 若是咱們但願在全部線程執行完畢之後打印當前計數的值。只須要log.info以前執行上一步便可countDownLatch.await();。 100 log.info("count:{}", count.get()); 101 102 } 103 104 }
7.二、線程安全性的原子性的使用,以下所示:
atomic包裏面AtomicLong類,調用了Unsafe類實現自增操做。jdk1.8新增了LongAddder類比AtomicLong類。this.compareAndSwapLong()方法核心就是CAS的核心。CAS實現的原理是拿當前的對象和底層裏面的值進行對比,若是當前對象的值和底層的值一致的時候才執行對應的加一操做。
1 package com.bie.concurrency.atomic; 2 3 import java.util.concurrent.CountDownLatch; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 import java.util.concurrent.Semaphore; 7 import java.util.concurrent.atomic.AtomicInteger; 8 import java.util.concurrent.atomic.AtomicLong; 9 10 import com.bie.concurrency.annoations.ThreadSafe; 11 12 import lombok.extern.slf4j.Slf4j; 13 14 /** 15 * 16 * 17 * @Title: CountDownLatchTest.java 18 * @Package com.bie.concurrency.test 19 * @Description: TODO 20 * @author biehl 21 * @date 2020年1月2日 22 * @version V1.0 23 * 24 * 併發模擬測試的程序。 25 * 26 * 一、CountDownLatch計數器向下減的閉鎖類。該類能夠阻塞線程,並保證線程在知足某種特定的條件下繼續執行。 27 * CountDownLatch比較適合咱們保證線程執行完以後再繼續其餘的處理。 28 * 29 * 二、Semaphore信號量,實現的功能是能夠阻塞進程而且控制同一時間的請求的併發量。 Semaphore更適合控制同時併發的線程數。 30 * 31 * 三、CountDownLatch、Semaphore配合線程池一塊兒使用。 32 * 33 * 四、jdk提供了Atomic包,來實現原子性,Atomic包裏面提供了不少AtomicXXX類,他們都是經過CAS來完成原子性的。 34 * 35 * 五、atomic包裏面AtomicLong類,調用了Unsafe類實現自增操做。jdk1.8新增了LongAddder類比AtomicLong類。 36 * 37 * unsafe.getAndAddLong(this, valueOffset, 1L) + 1L; 38 * this.compareAndSwapLong()方法核心就是CAS的核心。CAS實現的原理是拿當前的對象和底層裏面的值進行對比,若是當前對象的值和底層的值一致的時候才執行對應的加一操做。 39 * 40 */ 41 @Slf4j 42 @ThreadSafe // 因爲每次結果一致,因此是線程安全的類。可使用此程序進行併發測試。 43 public class ConcurrencyAtomicExample2 { 44 45 public static int clientTotal = 5000;// 5000個請求,請求總數 46 47 public static int threadTotal = 200;// 容許同時併發執行的線程數目 48 49 // int基本數據類型對應的atomic包裏面的類是AtomicInteger類型的。 50 // 初始化值爲0 51 public static AtomicLong count = new AtomicLong(0);// 計數的值 52 53 // 自增計數器 54 private static void add() { 55 // 自增操做調用的方法,類比++i 56 count.incrementAndGet(); 57 // 或者調用下面的方法,類比i++ 58 // count.getAndIncrement(); 59 } 60 61 public static void main(String[] args) { 62 // 定義線程池 63 ExecutorService executorService = Executors.newCachedThreadPool(); 64 // 定義信號量,信號量裏面須要定義容許併發的數量 65 final Semaphore semaphore = new Semaphore(threadTotal); 66 // 定義計數器閉鎖,但願全部請求完之後統計計數結果,將計數結果放入 67 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); 68 // 放入請求操做 69 for (int i = 0; i < clientTotal; i++) { 70 // 全部請求放入到線程池結果中 71 executorService.execute(() -> { 72 // 在線程池執行的時候引入了信號量,信號量每次作acquire()操做的時候就是判斷當前進程是否容許被執行。 73 // 若是達到了必定併發數的時候,add方法可能會臨時被阻塞掉。當acquire()能夠返回值的時候,add方法能夠被執行。 74 // add方法執行完畢之後,釋放當前進程,此時信號量就已經引入完畢了。 75 // 在引入信號量的基礎上引入閉鎖機制。countDownLatch 76 try { 77 // 執行核心執行方法以前引入信號量,信號量每次容許執行以前須要調用方法acquire()。 78 semaphore.acquire(); 79 // 核心執行方法。 80 add(); 81 // 核心執行方法執行完成之後,須要釋放當前進程,釋放信號量。 82 semaphore.release(); 83 } catch (InterruptedException e) { 84 e.printStackTrace(); 85 } 86 // try-catch是一次執行系統的操做,執行完畢之後調用一下閉鎖。 87 // 每次執行完畢之後countDownLatch裏面對應的計算值減一。 88 // 執行countDown()方法計數器減一。 89 countDownLatch.countDown(); 90 }); 91 } 92 // 這個方法能夠保證以前的countDownLatch必須減爲0,減爲0的前提就是全部的進程必須執行完畢。 93 try { 94 // 調用await()方法當前進程進入等待狀態。 95 countDownLatch.await(); 96 } catch (InterruptedException e) { 97 e.printStackTrace(); 98 } 99 // 一般,線程池執行完畢之後,線程池再也不使用,記得關閉線程池 100 executorService.shutdown(); 101 // 若是咱們但願在全部線程執行完畢之後打印當前計數的值。只須要log.info以前執行上一步便可countDownLatch.await();。 102 log.info("count:{}", count.get()); 103 104 } 105 106 }
7.三、LongAdder類和AtomicLong類。jdk1.8新增了LongAddder,新增的類,確定是有優勢的。
1)、AtomicInteger(CAS的實現原理)實現原理在死循環內裏面不斷進行循環修改目標值,在競爭不激烈的時候,修改爲功的機率很高,可是在競爭激烈的時候,修改失敗的機率很高,修改失敗之後進行循環操做,直到修改爲功,是十分影響性能。對於普通類型的long,double變量,jvm容許將64位的讀操做或者寫操做拆分紅2個32位的操做。
2)、LongAddder類的優勢,核心是將熱點數據分離,能夠將AtomicLong內部核心數據value分離成一個數組,每一個線程訪問的時候,經過hash等算法,映射到其中一個數字進行計數,最終的計數結果則爲這個數組的求和累加,其中熱點數據value會被分離成多個單元的sell,每一個sell獨自維護內部的值,當前對象實際的值由全部sell累加合成,這樣的話,熱點就進行了有效的分離並提升了並行度,這樣一來LongAddder至關因而在AtomicLong的基礎上將單點的更新壓力分散到各個節點上,在低併發的時候,經過對bash的直接更新能夠很好的保證和Atomic的性能基本一致,而在高併發的時候,則經過分散提升了性能。
3)、LongAddder類的缺點,統計的時候,若是有併發更新,可能會致使統計的數據出現偏差。
4)、實際使用中,在處理高併發計算的s時候,咱們能夠優先使用LongAdder類,而不是繼續使用AtomicLong。固然了,在線程競爭很低的狀況下進行計數,使用Atomic仍是更簡單,更直接一些,而且效果會更高一些。其餘的狀況下,好比序列號生成,這種狀況下須要準確的數據,全局惟一的AtomicLong纔是正確的選擇,此時不適合使用LongAdder類。
1 package com.bie.concurrency.atomic; 2 3 import java.util.concurrent.CountDownLatch; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 import java.util.concurrent.Semaphore; 7 import java.util.concurrent.atomic.LongAdder; 8 9 import com.bie.concurrency.annoations.ThreadSafe; 10 11 import lombok.extern.slf4j.Slf4j; 12 13 /** 14 * 15 * 16 * @Title: CountDownLatchTest.java 17 * @Package com.bie.concurrency.test 18 * @Description: TODO 19 * @author biehl 20 * @date 2020年1月2日 21 * @version V1.0 22 * 23 * 併發模擬測試的程序。 24 * 25 * 一、CountDownLatch計數器向下減的閉鎖類。該類能夠阻塞線程,並保證線程在知足某種特定的條件下繼續執行。 26 * CountDownLatch比較適合咱們保證線程執行完以後再繼續其餘的處理。 27 * 28 * 二、Semaphore信號量,實現的功能是能夠阻塞進程而且控制同一時間的請求的併發量。 Semaphore更適合控制同時併發的線程數。 29 * 30 * 三、CountDownLatch、Semaphore配合線程池一塊兒使用。 31 * 32 * 四、jdk提供了Atomic包,來實現原子性,Atomic包裏面提供了不少AtomicXXX類,他們都是經過CAS來完成原子性的。 33 * 34 * 五、jdk1.8新增了LongAddder,類比AtomicLong類。 35 * 36 * AtomicInteger(CAS的實現原理)實現原理在死循環內裏面不斷進行循環修改目標值,直到修改爲功,影響性能。 37 * 38 * LongAddder類的優勢,核心是將熱點數據分離。 39 * 40 * LongAddder類的缺點,統計的時候,若是有併發更新,會出現偏差。 41 * 42 */ 43 @Slf4j 44 @ThreadSafe // 因爲每次結果一致,因此是線程安全的類。可使用此程序進行併發測試。 45 public class ConcurrencyAtomicExample3 { 46 47 public static int clientTotal = 5000;// 5000個請求,請求總數 48 49 public static int threadTotal = 200;// 容許同時併發執行的線程數目 50 51 // int基本數據類型對應的atomic包裏面的類是AtomicInteger類型的。 52 // 初始化值爲0 53 public static LongAdder count = new LongAdder();// 計數的值,LongAdder默認值是0。 54 55 // 自增計數器 56 private static void add() { 57 // 自增操做調用的方法,類比++i 58 count.increment(); 59 } 60 61 public static void main(String[] args) { 62 // 定義線程池 63 ExecutorService executorService = Executors.newCachedThreadPool(); 64 // 定義信號量,信號量裏面須要定義容許併發的數量 65 final Semaphore semaphore = new Semaphore(threadTotal); 66 // 定義計數器閉鎖,但願全部請求完之後統計計數結果,將計數結果放入 67 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); 68 // 放入請求操做 69 for (int i = 0; i < clientTotal; i++) { 70 // 全部請求放入到線程池結果中 71 executorService.execute(() -> { 72 // 在線程池執行的時候引入了信號量,信號量每次作acquire()操做的時候就是判斷當前進程是否容許被執行。 73 // 若是達到了必定併發數的時候,add方法可能會臨時被阻塞掉。當acquire()能夠返回值的時候,add方法能夠被執行。 74 // add方法執行完畢之後,釋放當前進程,此時信號量就已經引入完畢了。 75 // 在引入信號量的基礎上引入閉鎖機制。countDownLatch 76 try { 77 // 執行核心執行方法以前引入信號量,信號量每次容許執行以前須要調用方法acquire()。 78 semaphore.acquire(); 79 // 核心執行方法。 80 add(); 81 // 核心執行方法執行完成之後,須要釋放當前進程,釋放信號量。 82 semaphore.release(); 83 } catch (InterruptedException e) { 84 e.printStackTrace(); 85 } 86 // try-catch是一次執行系統的操做,執行完畢之後調用一下閉鎖。 87 // 每次執行完畢之後countDownLatch裏面對應的計算值減一。 88 // 執行countDown()方法計數器減一。 89 countDownLatch.countDown(); 90 }); 91 } 92 // 這個方法能夠保證以前的countDownLatch必須減爲0,減爲0的前提就是全部的進程必須執行完畢。 93 try { 94 // 調用await()方法當前進程進入等待狀態。 95 countDownLatch.await(); 96 } catch (InterruptedException e) { 97 e.printStackTrace(); 98 } 99 // 一般,線程池執行完畢之後,線程池再也不使用,記得關閉線程池 100 executorService.shutdown(); 101 // 若是咱們但願在全部線程執行完畢之後打印當前計數的值。只須要log.info以前執行上一步便可countDownLatch.await();。 102 log.info("count:{}", count); 103 104 } 105 106 }
7.四、AtomicReference類提供了一個能夠原子讀寫的對象引用變量。原子意味着嘗試更改相同AtomicReference的多個線程(例如,使用比較和交換操做)不會使AtomicReference最終達到不一致的狀態。AtomicReference甚至有一個先進的compareAndSet()方法,它能夠將引用與預期值(引用)進行比較,若是它們相等,則在AtomicReference對象內設置一個新的引用。
1 package com.bie.concurrency.atomic; 2 3 import java.util.concurrent.atomic.AtomicReference; 4 5 import com.bie.concurrency.annoations.ThreadSafe; 6 7 import lombok.extern.slf4j.Slf4j; 8 9 /** 10 * 11 * 12 * @Title: CountDownLatchTest.java 13 * @Package com.bie.concurrency.test 14 * @Description: TODO 15 * @author biehl 16 * @date 2020年1月2日 17 * @version V1.0 18 * 19 * AtomicReference類提供了一個能夠原子讀寫的對象引用變量。 20 * 21 * 原子意味着嘗試更改相同AtomicReference的多個線程(例如,使用比較和交換操做)不會使AtomicReference最終達到不一致的狀態。 22 * 23 * AtomicReference甚至有一個先進的compareAndSet()方法,它能夠將引用與預期值(引用)進行比較,若是它們相等,則在AtomicReference對象內設置一個新的引用。 24 * 25 * 26 */ 27 @Slf4j 28 @ThreadSafe // 因爲每次結果一致,因此是線程安全的類。可使用此程序進行併發測試。 29 public class ConcurrencyAtomicExample4 { 30 31 // 默認值0 32 // 33 private static AtomicReference<Integer> count = new AtomicReference<Integer>(0); 34 35 public static void main(String[] args) { 36 count.compareAndSet(0, 2); // count = 2 37 count.compareAndSet(0, 1); // 不執行,由於此時參數一不是0哦,其餘類比同樣的。 38 count.compareAndSet(1, 3); // 不執行 39 count.compareAndSet(2, 4); // count = 4 40 count.compareAndSet(3, 5); // 不執行 41 log.info("count: {}", count.get()); 42 } 43 44 }
7.五、AtomicIntegerFieldUpdater核心是原子性的去更新某一個類的實例,指定的某一個字段。字段必須經過volatile修飾的。
1 package com.bie.concurrency.atomic; 2 3 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; 4 import java.util.concurrent.atomic.AtomicReference; 5 6 import com.bie.concurrency.annoations.ThreadSafe; 7 8 import lombok.Getter; 9 import lombok.extern.slf4j.Slf4j; 10 11 /** 12 * 13 * 14 * @Title: CountDownLatchTest.java 15 * @Package com.bie.concurrency.test 16 * @Description: TODO 17 * @author biehl 18 * @date 2020年1月2日 19 * @version V1.0 20 * 21 * AtomicIntegerFieldUpdater核心是原子性的去更新某一個類的實例,指定的某一個字段。字段必須經過volatile修飾的。 22 * 23 */ 24 @Slf4j 25 @ThreadSafe // 因爲每次結果一致,因此是線程安全的類。可使用此程序進行併發測試。 26 public class ConcurrencyAtomicExample5 { 27 28 // 須要本身定義一個字段名稱的變量,必須使用volatile關鍵字進行修飾。 29 @Getter 30 private volatile int count = 100; 31 32 // ConcurrencyAtomicExample5是更新的對象 33 // 參數1是ConcurrencyAtomicExample5類對象的class 34 // 參數2是對於的字段名稱 35 private static AtomicIntegerFieldUpdater<ConcurrencyAtomicExample5> updater = AtomicIntegerFieldUpdater 36 .newUpdater(ConcurrencyAtomicExample5.class, "count"); 37 38 // 定義一個實例,裏面包含了上面定義的字段count,其值是100. 39 // private static ConcurrencyAtomicExample5 concurrencyAtomicExample5 = new 40 // ConcurrencyAtomicExample5(); 41 42 public static void main(String[] args) { 43 ConcurrencyAtomicExample5 concurrencyAtomicExample5 = new ConcurrencyAtomicExample5(); 44 45 // 若是concurrencyAtomicExample5實例裏面的值是100,就更新爲120 46 if (updater.compareAndSet(concurrencyAtomicExample5, 100, 120)) { 47 log.info("update success 1 : {} ", concurrencyAtomicExample5.getCount()); 48 } 49 50 if (updater.compareAndSet(concurrencyAtomicExample5, 100, 120)) { 51 log.info("update success 2 : {} ", concurrencyAtomicExample5.getCount()); 52 } else { 53 log.info("update failed : {} ", concurrencyAtomicExample5.getCount()); 54 } 55 } 56 57 }
7.六、AtomicBoolean演示了某段代碼只會執行一次,不會出現重複的狀況。
1 package com.bie.concurrency.atomic; 2 3 import java.util.concurrent.CountDownLatch; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 import java.util.concurrent.Semaphore; 7 import java.util.concurrent.atomic.AtomicBoolean; 8 9 import com.bie.concurrency.annoations.ThreadSafe; 10 11 import lombok.extern.slf4j.Slf4j; 12 13 /** 14 * 15 * 16 * @Title: CountDownLatchTest.java 17 * @Package com.bie.concurrency.test 18 * @Description: TODO 19 * @author biehl 20 * @date 2020年1月2日 21 * @version V1.0 22 * 23 * 一、AtomicStampReference,解決CAS的ABA問題。compareAndSet該方法。s 24 * 25 * 1.一、ABA問題就是CAS在操做的時候,其餘線程將變量的值A修改爲了B,又改會了A。 26 * 本線程使用指望值A與當前變量進行比較的時候,發現A變量沒有改變。 27 * 因而CAS就將A值進行了交換操做。其實此時該值已經被其餘線程改變過了,這與設計思想是不符合的。 28 * 29 * 1.二、ABA問題解決思路是每次變量更新的時候,把變量的版本號加一,那麼以前將變量的值A修改爲了B,又改會了A,版本號修改了三次。 30 * 此時,只要某一個變量被線程修改了,該變量對應的版本號就會發生遞增變化,從而解決了ABA問題。 31 * 32 * 二、AtomicLongArray,維護的是一個數組。這個數組能夠選擇性的更新某一個索引對應的值,也是進行原子性操做的,相比於AtomicLong,AtomicLongArray會多一個索引值去更新。 33 * 34 * 三、AtomicBoolean演示了某段代碼只會執行一次,不會出現重複的狀況。 35 * 36 */ 37 @Slf4j 38 @ThreadSafe // 因爲每次結果一致,因此是線程安全的類。可使用此程序進行併發測試。 39 public class ConcurrencyAtomicExample6 { 40 41 public static int clientTotal = 5000;// 5000個請求,請求總數 42 43 public static int threadTotal = 200;// 容許同時併發執行的線程數目 44 45 public static AtomicBoolean isHappened = new AtomicBoolean();// 46 47 // 原子性操做,false變成true只會執行一次。剩下的4999次都沒有執行。 48 private static void test() { 49 // 若是當前值是false,將其變成true。 50 if (isHappened.compareAndSet(false, true)) { 51 log.info("execute"); 52 } 53 } 54 55 public static void main(String[] args) { 56 // 定義線程池 57 ExecutorService executorService = Executors.newCachedThreadPool(); 58 // 定義信號量,信號量裏面須要定義容許併發的數量 59 final Semaphore semaphore = new Semaphore(threadTotal); 60 // 定義計數器閉鎖,但願全部請求完之後統計計數結果,將計數結果放入 61 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); 62 // 放入請求操做 63 for (int i = 0; i < clientTotal; i++) { 64 // 全部請求放入到線程池結果中 65 executorService.execute(() -> { 66 // 在線程池執行的時候引入了信號量,信號量每次作acquire()操做的時候就是判斷當前進程是否容許被執行。 67 // 若是達到了必定併發數的時候,add方法可能會臨時被阻塞掉。當acquire()能夠返回值的時候,add方法能夠被執行。 68 // add方法執行完畢之後,釋放當前進程,此時信號量就已經引入完畢了。 69 // 在引入信號量的基礎上引入閉鎖機制。countDownLatch 70 try { 71 // 執行核心執行方法以前引入信號量,信號量每次容許執行以前須要調用方法acquire()。 72 semaphore.acquire(); 73 // 核心執行方法。 74 test(); 75 // 核心執行方法執行完成之後,須要釋放當前進程,釋放信號量。 76 semaphore.release(); 77 } catch (InterruptedException e) { 78 e.printStackTrace(); 79 } 80 // try-catch是一次執行系統的操做,執行完畢之後調用一下閉鎖。 81 // 每次執行完畢之後countDownLatch裏面對應的計算值減一。 82 // 執行countDown()方法計數器減一。 83 countDownLatch.countDown(); 84 }); 85 } 86 // 這個方法能夠保證以前的countDownLatch必須減爲0,減爲0的前提就是全部的進程必須執行完畢。 87 try { 88 // 調用await()方法當前進程進入等待狀態。 89 countDownLatch.await(); 90 } catch (InterruptedException e) { 91 e.printStackTrace(); 92 } 93 // 一般,線程池執行完畢之後,線程池再也不使用,記得關閉線程池 94 executorService.shutdown(); 95 // 若是咱們但願在全部線程執行完畢之後打印當前計數的值。只須要log.info以前執行上一步便可countDownLatch.await();。 96 log.info("isHappened:{}", isHappened.get()); 97 } 98 99 }
八、原子性提供了互斥訪問,同一時刻,只能有一個線程來對它進行操做。同一時刻只能有一個線程來對它進行操做,除了atomic包裏面的類,還有鎖,jdk提供鎖主要分兩種。特別注意,volatile是不具有原子性的。
1)、一種是synchronized(依賴JVM)。是java的關鍵字,主要依賴jvm來實現鎖機制。所以在這個關鍵字做用對象的做用範圍內都是同一時刻只能有一個線程能夠進行操做的,切記是做用對象的做用範圍內。
2)、另一種鎖是jdk提供的代碼層面的鎖Lock(Lock是接口)。依賴特殊的CPU指令,代碼實現,ReentrantLock。
3)、synchronized是java中的一個關鍵字,是一種同步鎖,修飾的對象主要有四種。
第一種,修飾代碼塊,被修飾的代碼稱爲同步語句塊,做用範圍是大括號括起來的代碼,做用對象是調用的對象。
第二種,修飾方法,被修飾的方法稱爲同步方法,做用範圍是整個方法,做用對象是調用這個方法的對象。
第三種,修飾靜態方法,做爲範圍是整個靜態方法,做用的對象是這個類的全部對象。
第四種,修飾類,做用範圍是synchronized後面括號括起來的部分,做用對象是這個類的全部對象。
8.一、第一種,修飾代碼塊,被修飾的代碼稱爲同步語句塊,做用範圍是大括號括起來的代碼,做用對象是調用的對象。第二種,修飾方法,被修飾的方法稱爲同步方法,做用範圍是整個方法,做用對象是調用這個方法的對象。
1 package com.bie.concurrency.example.sync; 2 3 import java.util.concurrent.ExecutorService; 4 import java.util.concurrent.Executors; 5 6 import lombok.extern.slf4j.Slf4j; 7 8 /** 9 * 10 * 11 * @Title: SynchronizedExample1.java 12 * @Package com.bie.concurrency.example.sync 13 * @Description: TODO 14 * @author biehl 15 * @date 2020年1月3日 16 * @version V1.0 17 * 18 * 一、若是一個方法內部是完整的同步代碼塊,那麼它和用synchronized修飾的方法是等同的。 19 * 由於整個實際中須要執行的代碼都是被synchronized修飾的。 20 * 21 * 二、若是SynchronizedExample1是父類,子類繼承了該類,若是調用codeMethod方法,是帶不上synchronized的。 22 * 由於synchronized不屬於方法聲明的一部分,是不能繼承的。若是子類也須要使用synchronized,須要本身顯示聲明的。 23 */ 24 @Slf4j 25 public class SynchronizedExample1 { 26 27 // synchronized修飾代碼塊 28 // 第一種,修飾代碼塊,被修飾的代碼稱爲同步語句塊,做用範圍是大括號括起來的代碼,做用對象是調用的對象。 29 // 對於同步代碼塊,做用於的是當前對象,對於不一樣調用對象是互相不影響的。 30 public void codeBlock(int j) { 31 // 做用範圍是大括號括起來的代碼 32 synchronized (this) { 33 for (int i = 0; i < 10; i++) { 34 log.info("codeBlock {} - {} ", j, i); 35 } 36 } 37 } 38 39 // synchronized修飾一個方法。 40 // 第二種,修飾方法,被修飾的方法稱爲同步方法,做用範圍是整個方法,做用對象是調用這個方法的對象。 41 // 修飾方法,被修飾的方法稱爲同步方法。 42 // 做用範圍是整個方法。 43 // 對於synchronized修飾方法,做用於調用對象的,對於不一樣調用對象是互相不影響的。 44 public synchronized void codeMethod(int j) { 45 for (int i = 0; i < 10; i++) { 46 log.info("codeMethod {} - {} ", j, i); 47 } 48 } 49 50 public static void main(String[] args) { 51 SynchronizedExample1 example1 = new SynchronizedExample1(); 52 SynchronizedExample1 example2 = new SynchronizedExample1(); 53 // 聲明一個線程池 54 ExecutorService executorService = Executors.newCachedThreadPool(); 55 // 開啓進程去執行這個方法。 56 executorService.execute(() -> { 57 // 第一種,修飾代碼塊,被修飾的代碼稱爲同步語句塊,做用範圍是大括號括起來的代碼,做用對象是調用的對象。 58 // example1.codeBlock(1); 59 60 example1.codeMethod(1); 61 }); 62 63 // 開啓進程去執行這個方法。 64 executorService.execute(() -> { 65 // 第二種,修飾方法,被修飾的方法稱爲同步方法,做用範圍是整個方法,做用對象是調用這個方法的對象。 66 // example1.codeBlock(2); 67 68 // example1.codeMethod(2); 69 70 // example2.codeBlock(2); 71 72 example2.codeMethod(2); 73 }); 74 75 } 76 77 }
8.二、第三種,修飾靜態方法,做爲範圍是整個靜態方法,做用的對象是這個類的全部對象。第四種,修飾類,做用範圍是synchronized後面括號括起來的部分,做用對象是這個類的全部對象。
1 package com.bie.concurrency.example.sync; 2 3 import java.util.concurrent.ExecutorService; 4 import java.util.concurrent.Executors; 5 6 import lombok.extern.slf4j.Slf4j; 7 8 /** 9 * 10 * 11 * @Title: SynchronizedExample1.java 12 * @Package com.bie.concurrency.example.sync 13 * @Description: TODO 14 * @author biehl 15 * @date 2020年1月3日 16 * @version V1.0 17 * 18 * 一、一個方法裏面若是全部須要執行的代碼部分都是被synchronized修飾的一個類來包圍的時候, 19 * 那麼它和synchronized修飾的靜態方法的表現是一致的。 20 */ 21 @Slf4j 22 public class SynchronizedExample2 { 23 24 // 第四種,修飾類,做用範圍是synchronized後面括號括起來的部分,做用對象是這個類的全部對象。 25 public void codeClass(int j) { 26 synchronized (SynchronizedExample2.class) { 27 for (int i = 0; i < 10; i++) { 28 log.info("codeBlock {} - {} ", j, i); 29 } 30 } 31 } 32 33 // 第三種,修飾靜態方法,做爲範圍是整個靜態方法,做用的對象是這個類的全部對象。 34 // 使用不一樣的類來調用靜態方法,調用被synchronized修飾的靜態方法的時候,同一個時間只容許一個線程能夠被調用執行。 35 // 使用synchronized修飾靜態方法,全部類之間都是原子性操做,同一個時間只容許一個線程能夠被調用執行。 36 public static synchronized void codeStaticMethod(int j) { 37 for (int i = 0; i < 10; i++) { 38 log.info("codeMethod {} - {} ", j, i); 39 } 40 } 41 42 @SuppressWarnings("static-access") 43 public static void main(String[] args) { 44 SynchronizedExample2 example1 = new SynchronizedExample2(); 45 SynchronizedExample2 example2 = new SynchronizedExample2(); 46 // 聲明一個線程池 47 ExecutorService executorService = Executors.newCachedThreadPool(); 48 // 開啓進程去執行這個方法。 49 executorService.execute(() -> { 50 // example1.codeStaticMethod(1); 51 52 example1.codeClass(1); 53 }); 54 55 // 開啓進程去執行這個方法。 56 executorService.execute(() -> { 57 // example1.codeStaticMethod(2); 58 59 example2.codeClass(1); 60 }); 61 62 } 63 64 }
九、可見性是一個線程對主內存的修改,能夠及時的被其餘線程觀察到,提及可見性,何時會致使不可見呢?致使共享變量在線程間不可見的緣由,主要有下面三個方面。對於可見性,JVM提供了synchronized、volatile。
1)、方面1、線程交叉執行。
2)、方面2、重排序結合線程交叉執行。
3)、方面3、共享變量更新後的值沒有在工做內存與主內存間及時更新。
十、對於可見性,JVM(java內存模型)提供了synchronized、volatile。可見性,JVM提供的synchronized。JMM關於synchronized的兩條規定,以下所示:
1)、線程解鎖前,必須把共享變量的最新值刷新到主內存。
2)、線程加鎖時候,將清空工做內存中共享變量的值,從而使用共享變量時須要從主內存中從新讀取最新的值(注意,加鎖和解鎖是同一把鎖)。
注意:在原子性裏面,synchronized的四種修飾方法,修飾方法前兩條是針對於調用對象的,對於不一樣對象,鎖的範圍是不同的,此時,若是不是同一把鎖,互相以前是不影響的。正是由於有了synchronized的可見性,解決了咱們以前見到的原子性,所以咱們在作線程安全同步的時候,咱們只要使用synchronized進行修飾以後,咱們的變量能夠放心的進行使用。
十一、可見性,volatile,經過加入內存屏障和禁止重排序優化來實現可見性的。
1)、對volatile變量寫操做的時候,會在寫操做後加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存中。
2)、對volatile變量讀操做的時候,會在讀操做前加入一條load屏障指令,從主內存中讀取共享變量。
注意:這兩點,通俗的說,volatile變量在每次線程訪問的時候,都強迫從主內存中讀取該變量的值,而當該變量發生變化的時候,又會強迫線程將最新的值刷新到主內存中,這樣的話,任什麼時候候不一樣的線程總能看到該變量的最新值。
特別注意,volatile是不具有原子性的。因此volatile是不適合計算場景的。那麼volatile適合什麼場景呢,使用volatile必須具有兩個條件,第一個是對變量的寫操做不依賴於當前值,第二個是該變量沒有包含在具備其餘變量不變的式子中。因此volatile很適合狀態標記量。另一個使用場景就是doubleCheck即檢查兩次場景。
十二、volatile讀操做,寫操做插入內存屏障和禁止重排序的示意圖。
1)、volatile寫操做,插入Store屏障的示意圖。對遇到volatile寫操做時,首先會在volatile寫以前插入一個StoreStore屏障(其做用是禁止上面的普通寫和下面的volatile寫重排序),以後會在volatile寫插入一個StoreLoad屏障(其做用是防止上面的volatile寫和下面可能有的volatile讀/寫重排序)。
1三、volatile讀操做,插入Load屏障的示意圖。對遇到volatile讀操做時,會插入Load屏障,首先是插入一個LoadLoad屏障(其做用是禁止下面全部普通操做和上面的volatile讀重排序),接下來插入LoadStore屏障(其做用是禁止下面全部的寫操做和上面的volatile讀重排序)。全部這些都是在CPU指令級別進行操做的,所以當使用volatile的時候已經具有了當前所說的這些規範。
1四、線程安全性裏面的有序性,Java內存模型中,容許編譯器和處理器對指令進行重排序,可是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。在Java中可使用volatile保證必定的有序性,另外也可使用synchronized和lock保證必定的有序性,很顯然synchronized和lock保證每一個時刻是有一個線程執行同步代碼,至關因而讓線程順序執行同步代碼,天然就保證了有序性。另外呢,Java內存模型具有先天的有序性,即不須要任何手段保證有序性,這個一般被稱爲happen-before原則,若是兩個操做的執行順序沒法從happen-before原則推導出來,他們就不能保證他們有序性了,虛擬機能夠隨意對他們進行重排序了。
1五、有序性,happens-before原則即先行發生原則,八條原則,以下所示:
1)、第一條:程序次序規則,一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。一段程序代碼的執行,在單個線程中,看起來是有序的,雖然這條規則中提到書寫在前面的操做先行發生於書寫在後面的操做,這個應該是程序看起來,執行的順序是按照代碼的順序執行的,由於虛擬機可能會對程序代碼進行指令重排序,雖然進行了重排序,可是最終執行的結果是與程序順序執行的結果是一致的,只會對不存在數據依賴的指令進行重排序,所以在單線程中程序執行看起來是有序的。事實上,這個規則是用來保證程序在單線程中執行結果的正確性,可是沒法保證程序在多線程中執行的正確性。
2)、第二條:鎖定規則,一個UnLock操做先行發生於後面對同一個鎖的lock操做。也就是說不管在單線程中仍是多線程中,同一個鎖若是處於被鎖定的狀態,那麼必須先對鎖進行釋放操做,後面才能繼續進行lock操做。
3)、第三條:volatile變量規則,對一個變量的寫操做先行發生於後面對這個變量的讀操做。若是一個線程先去寫一個變量,而後一個線程去進行讀取,那麼寫入操做確定會先行發生於讀操做。
4)、第四條:傳遞規則,若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C。
5)、第五條:線程啓動規則,Thread對象的start()方法先行發生於此線程的每個動做。一個Thread對象必須先執行start()方法才能作其餘的操做。
6)、第六條:線程中斷規則,對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。必須執行了interrupt()方法才能夠被檢測到中斷事件的發生。
7)、第七條:線程終結規則,線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行。
8)、第八條:對象終結規則,一個對象的初始化完成先行發生於他的finalize()方法的開始。
注意:若是兩個操做的執行次序,沒法從happens-before原則推導出來,就不能保證他們的有序性,虛擬機就能夠隨意的對他們進行重排序。
1六、線程安全性的總結。
1)、原子性,主要是提供了互斥訪問,同一時刻只能有一個線程ji進行操做。原子性裏面須要注意Atomic包、CAS算法、synchronized、Locl鎖。
2)、可見性,是指一個線程對主內存的修改,能夠及時的被其餘線程觀察到。在可見性裏面,須要注意synchronized、volatile關鍵字。
3)、有序性,主要介紹了happens-before原則,一個線程觀察q其餘線程中指令執行順序,因爲指令重排序的存在,這個觀察結果通常都會雜亂無序的。若是兩個操做的執行次序,沒法從happens-before原則推導出來,就不能保證他們的有序性,虛擬機就能夠隨意的對他們進行重排序。
1七、CPU的多級緩存。左側的圖展現的最簡單的高速緩存的配置,數據的讀取和存儲都通過高速緩存的,CPU核心與高速緩存之間是有一條特殊的快速通道,在這個簡化的圖裏面,主存與告訴緩存都鏈接在系統總線上,這條總線同時也用於其餘組件的通訊。右側的圖展現的是,在高速緩存出現後不久,系統變得更加複雜,高速緩存與主存之間的速度差別被拉大,直到加入了另外一級的緩存,新加入的這一緩存比第一緩存更大,可是更慢,因爲加大一級緩存的作飯從經濟上考慮是行不通的,因此有了二級緩存,甚至有的系統出現了三級緩存。
1八、爲何須要CPU cache緩存呢?
答:CPU的頻率太快了,快到主存跟不上,這樣在處理器時鐘週期內,CPU經常須要等待主存,浪費資源,因此cache的出現,是爲了緩解CPU和主存之間速度的不匹配問題(注意,結構如是,cpu -> cache緩存 -> memory主存)。
1九、CPU cache緩存有什麼意思呢,緩存的容量遠遠小於主存的,所以出現緩存不被命中的機率在所不免,既然緩存不能包含CPU所須要的全部數據,那麼緩存的存在到底有什麼意義呢?
1)、時間局部性,若是某個數據被訪問,那麼在不久的未來它極可能被再次訪問。
2)、空間局部性,若是某個數據被訪問,那麼與它相鄰的數據很快也可能被訪問的。
20、CPU多級緩存的緩存一致性(MESI,MESI協議是基於Invalidate的高速緩存一致性協議,而且是支持回寫高速緩存的最經常使用協議之一)。參考http://www.javashuo.com/article/p-qgwgeljq-r.html。
多核CPU的狀況下有多個一級緩存,如何保證緩存內部數據的一致,不讓系統數據混亂。這裏就引出了一個一致性的協議MESI。MESI協議用於保證多個CPU cache之間緩存共享數據的一致。MESI是指4中狀態的首字母,定義了每一個Cache line(緩存行,緩存存儲數據的單元)的4個狀態,可用2個bit表示。CPU對cache的四種操做可能會出現不一致的狀態,所以緩存控制器監聽到本地操做和遠程操做的時候,須要對Cache line作出必定的修改,從而保證數據在多個緩存之間流轉的一致性。
2一、MESI狀態轉換圖,以下所示:
local read、local write、remote read、remote write四種操做,以下所示:
MESI協議的Cache line數據狀態有四種,引發數據狀態轉換的cpu cache操做也是有四種的。若是要深入理解MESI協議,要深入理解16種轉換的狀況,狀態之間的相互轉換關係,以下所示:
在一個典型的多核系統中,每個核都會有本身的緩存,來共享主存總線,每一個響應的cpu會發出讀寫請求,而緩存的目的是減小CPU讀寫共享主存的次數,一個緩存除了在invalid狀態以外,均可以知足CPU的讀請求,一個寫請求,只有該緩存行在M狀態或者E狀態的時候,才能夠被執行,若是當前狀態是處於S狀態的時候,必須先將緩存中的緩存行變成無效的狀態,這個操做一般做用於廣播的方式來完成,這個時候既不容許不一樣的CPU來修改同一個緩存行,即便修改該緩存行不一樣的位置數據也是不容許的,這裏主要解決緩存一致性的問題。一個處於M狀態的緩存行必須時刻監聽全部試圖讀該緩存行相對主存的操做,這種操做必須在緩存將該緩存行寫回到主存,並將狀態變成S狀態以前被延遲執行。一個處於S狀態的緩存行也必須監聽其餘緩存使該緩存行無效,或者獨享該緩存行的請求並將緩存行變成I無效狀態。一個處於E狀態的緩存行要監聽其餘緩存讀緩存中該緩存行的操做,一旦有該緩存行的操做,那麼他須要變成S狀態。因此對於M和E狀態,它們的數據老是精確的,它們在和緩存行真正狀態是一致的,而S狀態多是非一致的,若是緩存將處於S狀態的緩存行做廢了,另外一個緩存實際上可能已經獨享了該緩存行,可是該緩存卻不會將緩存行升遷爲E狀態,這是由於其餘緩存不會廣播他們做廢掉該緩存行的通知,一樣,因爲緩存並無保存該緩存行的copy的數量,所以(即便有這種通知)也沒有辦法肯定本身是否已經獨享了該緩存行。從上面的意義看來E狀態是一種投機性的優化:若是一個CPU想修改一個處於S狀態的緩存行,總線事務須要將全部該緩存行的copy的值變成invalid狀態,而修改E狀態的緩存不須要使用總線事務。
2二、CPU多級緩存,亂序執行優化。
答:CPU多級緩存,亂序執行優化。處理器爲了提升運算速度而作出違背代碼原有順序的優化。可是計算過程,在正常狀況下,不會對結果形成影響的。在單核時代,處理器保證作出的優化不會致使執行的結果遠離預期目標,可是在多核環境下,並不是如此,多核時代,同時有多個核執行指令,每一個核的指令均可能被亂序,另外,處理器還引入了L1,L2等緩存機制,每一個核都有本身的緩存,這就致使了邏輯次序後寫入內存的未必真的最後寫入,最終致使了一個問題,若是咱們不作任何防禦措施,處理器最終獲得的結果和邏輯獲得的結果大不相同。
2三、Java虛擬機提供了Java內存模型(Java Memory Model,簡稱JMM)。
答:瞭解了CPU的緩存一致性、亂序執行優化,在多核多併發下須要額外作不少操做的,才能保證程序執行符合咱們的預期。
爲了屏蔽各類硬件和操做系統內存的訪問差別,以實現Java程序在各類平臺下都能達到一致的併發效果,Java虛擬機提供了Java內存模型(Java Memory Model,簡稱JMM)。JMM是一種規範,規範了Java虛擬機與計算機內存如何協同工做的,規定了一個線程如何和什麼時候能夠看到其餘線程修改事後的共享變量的值,以及必須時如何同步的訪問共享變量。
JVM內存分片的兩個概念,Heap堆、Stack棧。
1)、Heap堆,java裏面的堆是運行時的數據區,堆是由垃圾回收負責的,堆的優點是能夠動態的分配內存大小,生存期也沒必要事先告訴編譯器,由於它是在運行時動態分配內存的,java的垃圾收集器會自動搜索再也不使用數據,可是也有缺點,缺點就是因爲須要在運行時動態分配內存,所以它的存取速度相對慢一些。
2)、Stack棧,棧的優點是存取速度比堆要快,僅次於計算機裏面的寄存器,棧裏面的數據是能夠共享的,可是它的缺點存在棧中的數據大小與生存期必須是肯定的,缺少一些靈活性,棧中主要存在一些基本類型的變量。Java內存模型要求調用棧和本地變量存放在線程棧上,對象存放在堆上。
3)、一個本地變量多是指向一個對象的引用,這種狀況下,引用這個本地變量是存放在線程棧上的,可是對象自己是存放在堆上的。
4)、一個對象可能包含方法methodOne()、methodTwo(),這些方法可能包含本地變量,local variable一、local variable2,這些本地變量仍然是存放在線程棧上的,即便這些方法所屬的對象存儲在堆上。
5)、一個對象的成員變量可能會隨着這個對象自身存放在堆上,無論這個成員變量是原始類型仍是引用類型。
6)、靜態成員變量跟隨着類的定義一塊兒存放在堆上,存放在堆上的對象能夠被所持久對這個對象引用的線程訪問。
7)、若是Thead存放了Object的引用,是能夠訪問Object的,當一個線程能夠訪問一個對象的時候,此線程也能夠訪問這個對象的成員變量,當了兩個線程同時訪問一個對象的同一個方法,兩個線程會都訪問該對象的成員變量,可是每一個線程都擁有該對象成員變量的私有拷貝。如Thead Stack同時調用了Object3對象的methodOne()方法。
2四、計算機硬件架構簡單的圖示。以下所示:
1)、CPU簡介,如今的計算機一般有多個CPU,其中一些CPU還有多核,在有2個或者多個CPU的計算機上,同時運行多個線程是很是有可能的,並且每一個CPU在某一個時刻運行一個線程是確定沒有問題的,這就意味着,你的Java程序是多線程的,在你的java程序中,每一個CPU上一個線程多是併發執行的。
2)、CPU寄存器,CPU Registers,每一個CPU都包含一系列的寄存器,他們是CPU內存的基礎,CPU在寄存器上執行操做的速度遠遠大於在主存上執行的速度,這是由於CPU訪問寄存器的速度遠大於主存。
3)、CPU高速緩存,CPU Cache Memory,因爲計算機的存儲設備與處理器的預算速度之間有幾個數量級的差距,如今的計算機都加入了讀寫速度儘量接近處理器運算速度的高級緩存,來做爲內存與處理器之間的緩衝,將運算須要使用到的數據複製到緩存中,讓運算能夠快速的進行,當運算結束後,再從緩存同步到內存之中,這樣處理器就不用等待緩存的內存讀寫了,CPU訪問緩存層的速度快於訪問主存的速度,但一般比訪問內部寄存器的速度仍是要慢一點的,每一個CPU都有一個CPU的緩存層,一個CPU還有多層緩存,在某一時刻,一個或者多個緩存行,可能被讀到緩存,可能在被刷新回主存,同一時間點可能有多個操做在這裏面。
4)、內存,RAM-MAIN Memory,一個計算機還包含一個主存,全部的CPU均可以訪問主存,主存一般比CPU中的緩存大的多。
5)、運做原理,一般狀況下,當一個CPU須要讀取主存的時候呢,它會將主存的部分讀取到CPU緩存中,可能會將緩存中的部份內存讀取到CPU內部的寄存器裏面,而後再寄存器裏面執行操做,當CPU須要將結果回寫到主存的時候,它會將內部寄存器裏面的值刷新到緩存中,而後在某個時間點將值刷新到主存中。
2五、Java內存模型與硬件內存架構之間的一些關聯。
Java內存模型與硬件內存架構是存在一些差別的,硬件內存架構是沒有區分線程棧、堆、堆。對於硬件內存架構全部線程棧、堆都分佈在主內存中,部分線程棧和堆可能會出如今CPU緩存中,和CPU內部的寄存器裏面。
2六、Java內存模型抽象結構圖。
線程和主內存之間的抽象關係。線程之間的共享變量存儲在主內存中,每一個線程都有一個私有的本地內存,本地內存是Java內存模型的一個抽象概念,並非真實存在的,它涵蓋了緩存、寫緩衝區、寄存器以及其餘的硬件和編譯器的優化。本地內存中存儲了該線程以讀或者寫共享變量拷貝的一個副本,好比若是線程A要使用主內存中共享變量,先拷貝主內存中一個共享變量副本,放到本身的本地內存中,從耕地的層次來講,主內存就是硬件的內存,是爲了獲取更好的運行速度,虛擬機和硬件內存可能會讓工做內存優先存儲於寄存器和高速緩存中。Java內存模型中的線程中的工做內存是CPU的寄存器和高速緩存的一個抽象的描述,而JVM的靜態存儲模型(即JVM內存模型)只是一種對內存的物理劃分而已,只侷限於內存,並且只侷限於JVM的內存。如今線程之間通訊必需要通過主內存,若是線程A和線程B之間要進行通訊,那麼必須通過兩個步驟,第一步,線程A將本地的內存A中的更新過的共享變量刷新到主內存中去,線程B去主內存中讀取線程A已經更新過的共享變量。
2七、Java內存模型,同步的八種操做、以及Java內存模型的同步規則。
八種操做的概念解釋,以下所示:
1)、lock(鎖定):做用於主內存的變量,把一個變量標識爲一條線程獨佔變量。lock對應着unlock。
2)、unlock(解鎖):做用於主內存中的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。
3)、read(讀取):做用於主內存的變量,它把一個變量值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用。
4)、load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量的副本中。
5)、use(使用):做用於工做內存的變量,把工做內存中的一個變量的值傳給執行引擎。每當虛擬機遇到一個使用到變量的指令時都會使用該指令。
6)、assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量。每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
7)、store(存儲):做用於工做內存的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨後的write操做使用。
8)、write(寫入):做用於主內存的變量,它把store操做從工做內存中一個變量的值傳送到主內存的變量中。
Java內存模型的同步規則,以下所示:
1)、若是要把一個變量從主內存中複製到工做內存,就須要按照尋地執行read和Load操做,若是把變量從工做內存中同步回內存中,就要按照順序地執行store和write操做。可是Java內存模型只要求上述操做必須按照順序執行,而沒有保證必須是連續執行。
2)、不容許read和load、store和write操做之一單獨出現。由於它們實際上是一個連貫的動做,讀取和寫回。以上兩個操做必須按照順序執行,只有read完了才能夠load,只有store完了才能夠write,可是沒有保證必須是連續執行,read和load、store和write之間是能夠插入其餘指令的。
3)、不容許一個線程丟棄它的最近assign的操做,即變量在工做內存中改變了以後必須把變化同步到主內存中。
4)、不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從工做內存同步回主內存中。必須有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操做)。
2八、併發的優點與風險。
做者:別先生
博客園:https://www.cnblogs.com/biehongli/
若是您想及時獲得我的撰寫文章以及著做的消息推送,能夠掃描上方二維碼,關注我的公衆號哦。