Java併發編程與高併發之線程安全性(原子性、可見性、有序性)

1、併發的基本概念:同時擁有兩個或者多個線程,如果程序在單核處理器上運行,多個線程將交替地換入或者換出內存,這些線程是同時存在的,每個線程都處於執行過程中的某個狀態。如果允許在多核處理器上,此時程序中的每個線程都將分配到一個處理器核上,因此可以同時運行。併發,多個線程操作相同的資源,保證線程安全,合理利用資源。

2、高併發的概念:高併發(High Concurrency)是互聯網分佈式系統架構設計中必須考慮的因素之一,它通常是指,通過設計保證系統能夠同時並行處理很多請求。
  高併發,服務能同時處理很多請求,提高程序性能。主要是指系統運行過程中,短時間內,遇到大量操作請求的情況,主要發生在系統集中收到大量請求,比如12306的搶票,天貓雙十一的活動,這種情況的發生就會導致系統在這段時間內執行大量的操作,例如對資源的請求,數據庫的操作等等。

3、併發編程與線程安全:線程安全就是代碼所在的進程有多個線程在同時執行,而這些線程k可能會運行同一段代碼,如果每次運行結果和單線程運行結果一致,而且其他變量的值也和預期是一樣的,我們就認爲這是線程安全的,就是併發環境下得到我們期望的正確的結果。線程不安全不提供數據訪問保護,有可能出現多個線程先後更改數據造成所得到的數據出現髒數據,也可能在計算的時候出現錯誤。

4、併發模擬的幾種方式。

  第一種:Postman,Http請求模擬工具。
  第二種:Apache Bench(簡稱AB),Apache附帶的工具,測試網站性能。
  第三種:JMeter,Apache組織開發的壓力測試工具。
  第四種:Semaphore、CountDownLatch等等代碼進行併發模擬測試。

4.1、Postman,Http請求模擬工具,測試如下所示:

點擊Run以後,設置完畢參數開始執行。

執行完的效果如下所示:

4.2、Apache Bench(簡稱AB),Apache附帶的工具,測試網站性能。AB是一個命令行的工具,輸入命令就可以進行測試,對發起負載的本機要求很低,根據ab命令可以創建很多的併發訪問線程,模擬多個訪問者同時對同一個url地址進行訪問,因此可以用來測試目標服務器的負載壓力。

AB指定命令發送請求以後,可以得到每秒產生的字節數、每次處理請求的時間、每秒處理請求的數目等等統計數據。

安裝Apache服務器,官網下載地址:https://www.apachelounge.com/download/

1002211-20200102095117526-1898136252.pnguploading.4e448015.gif轉存失敗重新上傳取消

在D:\biehl\ApacheBench\Apache24\bin目錄下面找到ab.exe。-n是本次測試的總數,-c是指定本次併發數。

4.3、JMeter,Apache組織開發的壓力測試工具。官網地址:https://jmeter.apache.org/

在D:\biehl\JMeter\apache-jmeter-5.2.1\bin目錄下面執行jmeter.bat腳本文件。

4.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  *          1、CountDownLatch計數器向下減的閉鎖類。該類可以阻塞線程,並保證線程在滿足某種特定的條件下繼續執行。
25  *              CountDownLatch比較適合我們保證線程執行完之後再繼續其他的處理。
26  * 
27  *          2、Semaphore信號量,實現的功能是可以阻塞進程並且控制同一時間的請求的併發量。
28  *              Semaphore更適合控制同時併發的線程數。
29  * 
30  *          3、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 }

5、線程安全性。

  線程安全性的定義,當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些進程將如何交替執行,並且在主調代碼中不需要任何額外的同步或者協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。

6、線程安全性主要體現在三個方面原子性、可見性、有序性。

  a、原子性,提供了互斥訪問,同一時刻只能有一個線程來對它進行操作。
  b、可見性,一個線程對主內存的修改可以及時的被其他線程觀察到。
  c、有序性,一個線程觀察其他線程中的指令執行順序,由於指令重排序的存在,該觀察結果一般雜亂無序。

7、線程安全性的原子性的底層代碼理解,如下所示。

 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     // 裏面的三個參數對象下面方法的var1、var2、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是2、var5從底層傳遞的2、最後一個參數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.1、線程安全性的原子性的使用,如下所示:

  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  *          1、CountDownLatch計數器向下減的閉鎖類。該類可以阻塞線程,並保證線程在滿足某種特定的條件下繼續執行。
 26  *          CountDownLatch比較適合我們保證線程執行完之後再繼續其他的處理。
 27  * 
 28  *          2、Semaphore信號量,實現的功能是可以阻塞進程並且控制同一時間的請求的併發量。 Semaphore更適合控制同時併發的線程數。
 29  * 
 30  *          3、CountDownLatch、Semaphore配合線程池一起使用。
 31  * 
 32  *          4、jdk提供了Atomic包,來實現原子性,Atomic包裏面提供了很多AtomicXXX類,他們都是通過CAS來完成原子性的。
 33  * 
 34  *          5、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.2、線程安全性的原子性的使用,如下所示:

  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  *          1、CountDownLatch計數器向下減的閉鎖類。該類可以阻塞線程,並保證線程在滿足某種特定的條件下繼續執行。
 27  *          CountDownLatch比較適合我們保證線程執行完之後再繼續其他的處理。
 28  * 
 29  *          2、Semaphore信號量,實現的功能是可以阻塞進程並且控制同一時間的請求的併發量。 Semaphore更適合控制同時併發的線程數。
 30  * 
 31  *          3、CountDownLatch、Semaphore配合線程池一起使用。
 32  * 
 33  *          4、jdk提供了Atomic包,來實現原子性,Atomic包裏面提供了很多AtomicXXX類,他們都是通過CAS來完成原子性的。
 34  * 
 35  *          5、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.3、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  *          1、CountDownLatch計數器向下減的閉鎖類。該類可以阻塞線程,並保證線程在滿足某種特定的條件下繼續執行。
 26  *          CountDownLatch比較適合我們保證線程執行完之後再繼續其他的處理。
 27  * 
 28  *          2、Semaphore信號量,實現的功能是可以阻塞進程並且控制同一時間的請求的併發量。 Semaphore更適合控制同時併發的線程數。
 29  * 
 30  *          3、CountDownLatch、Semaphore配合線程池一起使用。
 31  * 
 32  *          4、jdk提供了Atomic包,來實現原子性,Atomic包裏面提供了很多AtomicXXX類,他們都是通過CAS來完成原子性的。
 33  * 
 34  *          5、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.4、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.5、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.6、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  *          1、AtomicStampReference,解決CAS的ABA問題。compareAndSet該方法。s
24  * 
25  *          1.1、ABA問題就是CAS在操作的時候,其他線程將變量的值A修改成了B,又改會了A。
26  *          本線程使用期望值A與當前變量進行比較的時候,發現A變量沒有改變。
27  *          於是CAS就將A值進行了交換操作。其實此時該值已經被其他線程改變過了,這與設計思想是不符合的。
28  * 
29  *          1.2、ABA問題解決思路是每次變量更新的時候,把變量的版本號加一,那麼之前將變量的值A修改成了B,又改會了A,版本號修改了三次。
30  *          此時,只要某一個變量被線程修改了,該變量對應的版本號就會發生遞增變化,從而解決了ABA問題。
31  * 
32  *          2、AtomicLongArray,維護的是一個數組。這個數組可以選擇性的更新某一個索引對應的值,也是進行原子性操作的,相比於AtomicLong,AtomicLongArray會多一個索引值去更新。
33  * 
34  *          3、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 }

8、原子性提供了互斥訪問,同一時刻,只能有一個線程來對它進行操作。同一時刻只能有一個線程來對它進行操作,除了atomic包裏面的類,還有鎖,jdk提供鎖主要分兩種。特別注意,volatile是不具備原子性的。

  1)、一種是synchronized(依賴JVM)。是java的關鍵字,主要依賴jvm來實現鎖機制。因此在這個關鍵字作用對象的作用範圍內都是同一時刻只能有一個線程可以進行操作的,切記是作用對象的作用範圍內。

  2)、另外一種鎖是jdk提供的代碼層面的鎖Lock(Lock是接口)。依賴特殊的CPU指令,代碼實現,ReentrantLock。

  3)、synchronized是java中的一個關鍵字,是一種同步鎖,修飾的對象主要有四種。
    第一種,修飾代碼塊,被修飾的代碼稱爲同步語句塊,作用範圍是大括號括起來的代碼,作用對象是調用的對象。
    第二種,修飾方法,被修飾的方法稱爲同步方法,作用範圍是整個方法,作用對象是調用這個方法的對象。
    第三種,修飾靜態方法,作爲範圍是整個靜態方法,作用的對象是這個類的所有對象。
    第四種,修飾類,作用範圍是synchronized後面括號括起來的部分,作用對象是這個類的所有對象。

8.1、第一種,修飾代碼塊,被修飾的代碼稱爲同步語句塊,作用範圍是大括號括起來的代碼,作用對象是調用的對象。第二種,修飾方法,被修飾的方法稱爲同步方法,作用範圍是整個方法,作用對象是調用這個方法的對象。

 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  *          1、如果一個方法內部是完整的同步代碼塊,那麼它和用synchronized修飾的方法是等同的。
19  *          因爲整個實際中需要執行的代碼都是被synchronized修飾的。 
20  *          
21  *          2、如果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.2、第三種,修飾靜態方法,作爲範圍是整個靜態方法,作用的對象是這個類的所有對象。第四種,修飾類,作用範圍是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  *          1、一個方法裏面如果所有需要執行的代碼部分都是被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 }

9、可見性是一個線程對主內存的修改,可以及時的被其他線程觀察到,說起可見性,什麼時候會導致不可見呢?導致共享變量在線程間不可見的原因,主要有下面三個方面。對於可見性,JVM提供了synchronized、volatile。

  1)、方面一、線程交叉執行。
  2)、方面二、重排序結合線程交叉執行。
  3)、方面三、共享變量更新後的值沒有在工作內存與主內存間及時更新。

10、對於可見性,JVM(java內存模型)提供了synchronized、volatile。可見性,JVM提供的synchronized。JMM關於synchronized的兩條規定,如下所示:

  1)、線程解鎖前,必須把共享變量的最新值刷新到主內存。
  2)、線程加鎖時候,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值(注意,加鎖和解鎖是同一把鎖)。

  注意:在原子性裏面,synchronized的四種修飾方法,修飾方法前兩條是針對於調用對象的,對於不同對象,鎖的範圍是不一樣的,此時,如果不是同一把鎖,互相之前是不影響的。正是因爲有了synchronized的可見性,解決了我們之前見到的原子性,因此我們在做線程安全同步的時候,我們只要使用synchronized進行修飾之後,我們的變量可以放心的進行使用。

11、可見性,volatile,通過加入內存屏障和禁止重排序優化來實現可見性的。

  1)、對volatile變量寫操作的時候,會在寫操作後加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存中。
  2)、對volatile變量讀操作的時候,會在讀操作前加入一條load屏障指令,從主內存中讀取共享變量。

  注意:這兩點,通俗的說,volatile變量在每次線程訪問的時候,都強迫從主內存中讀取該變量的值,而當該變量發生變化的時候,又會強迫線程將最新的值刷新到主內存中,這樣的話,任何時候不同的線程總能看到該變量的最新值。

  特別注意,volatile是不具備原子性的。所以volatile是不適合計算場景的。那麼volatile適合什麼場景呢,使用volatile必須具備兩個條件,第一個是對變量的寫操作不依賴於當前值,第二個是該變量沒有包含在具有其他變量不變的式子中。所以volatile很適合狀態標記量。另外一個使用場景就是doubleCheck即檢查兩次場景。

12、volatile讀操作,寫操作插入內存屏障和禁止重排序的示意圖。

  1)、volatile寫操作,插入Store屏障的示意圖。對遇到volatile寫操作時,首先會在volatile寫之前插入一個StoreStore屏障(其作用是禁止上面的普通寫和下面的volatile寫重排序),之後會在volatile寫插入一個StoreLoad屏障(其作用是防止上面的volatile寫和下面可能有的volatile讀/寫重排序)。

13、volatile讀操作,插入Load屏障的示意圖。對遇到volatile讀操作時,會插入Load屏障,首先是插入一個LoadLoad屏障(其作用是禁止下面所有普通操作和上面的volatile讀重排序),接下來插入LoadStore屏障(其作用是禁止下面所有的寫操作和上面的volatile讀重排序)。所有這些都是在CPU指令級別進行操作的,因此當使用volatile的時候已經具備了當前所說的這些規範。

14、線程安全性裏面的有序性,Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。在Java中可以使用volatile保證一定的有序性,另外也可以使用synchronized和lock保證一定的有序性,很顯然synchronized和lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼,自然就保證了有序性。另外呢,Java內存模型具備先天的有序性,即不需要任何手段保證有序性,這個通常被稱爲happen-before原則,如果兩個操作的執行順序無法從happen-before原則推導出來,他們就不能保證他們有序性了,虛擬機可以隨意對他們進行重排序了。

15、有序性,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原則推導出來,就不能保證他們的有序性,虛擬機就可以隨意的對他們進行重排序。

16、線程安全性的總結。

  1)、原子性,主要是提供了互斥訪問,同一時刻只能有一個線程ji進行操作。原子性裏面需要注意Atomic包、CAS算法、synchronized、Locl鎖。
  2)、可見性,是指一個線程對主內存的修改,可以及時的被其他線程觀察到。在可見性裏面,需要注意synchronized、volatile關鍵字。
  3)、有序性,主要介紹了happens-before原則,一個線程觀察q其他線程中指令執行順序,由於指令重排序的存在,這個觀察結果一般都會雜亂無序的。如果兩個操作的執行次序,無法從happens-before原則推導出來,就不能保證他們的有序性,虛擬機就可以隨意的對他們進行重排序。

 


 

17、CPU的多級緩存。左側的圖展示的最簡單的高速緩存的配置,數據的讀取和存儲都經過高速緩存的,CPU核心與高速緩存之間是有一條特殊的快速通道,在這個簡化的圖裏面,主存與告訴緩存都連接在系統總線上,這條總線同時也用於其他組件的通信。右側的圖展示的是,在高速緩存出現後不久,系統變得更加複雜,高速緩存與主存之間的速度差異被拉大,直到加入了另一級的緩存,新加入的這一緩存比第一緩存更大,但是更慢,由於加大一級緩存的做飯從經濟上考慮是行不通的,所以有了二級緩存,甚至有的系統出現了三級緩存。

18、爲什麼需要CPU cache緩存呢?

  答:CPU的頻率太快了,快到主存跟不上,這樣在處理器時鐘週期內,CPU常常需要等待主存,浪費資源,所以cache的出現,是爲了緩解CPU和主存之間速度的不匹配問題(注意,結構如是,cpu -> cache緩存 -> memory主存)。

19、CPU cache緩存有什麼意思呢,緩存的容量遠遠小於主存的,因此出現緩存不被命中的概率在所難免,既然緩存不能包含CPU所需要的所有數據,那麼緩存的存在到底有什麼意義呢?

  1)、時間局部性,如果某個數據被訪問,那麼在不久的將來它很可能被再次訪問。
  2)、空間局部性,如果某個數據被訪問,那麼與它相鄰的數據很快也可能被訪問的。

20、CPU多級緩存的緩存一致性(MESI,MESI協議是基於Invalidate的高速緩存一致性協議,並且是支持回寫高速緩存的最常用協議之一)。參考https://www.cnblogs.com/yanlong300/p/8986041.html

  多核CPU的情況下有多個一級緩存,如何保證緩存內部數據的一致,不讓系統數據混亂。這裏就引出了一個一致性的協議MESI。MESI協議用於保證多個CPU cache之間緩存共享數據的一致。MESI是指4中狀態的首字母,定義了每個Cache line(緩存行,緩存存儲數據的單元)的4個狀態,可用2個bit表示。CPU對cache的四種操作可能會出現不一致的狀態,因此緩存控制器監聽到本地操作和遠程操作的時候,需要對Cache line做出一定的修改,從而保證數據在多個緩存之間流轉的一致性。

21、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狀態的緩存不需要使用總線事務。 

22、CPU多級緩存,亂序執行優化。

  答:CPU多級緩存,亂序執行優化。處理器爲了提高運算速度而做出違背代碼原有順序的優化。但是計算過程,在正常情況下,不會對結果造成影響的。在單核時代,處理器保證做出的優化不會導致執行的結果遠離預期目標,但是在多核環境下,並非如此,多核時代,同時有多個核執行指令,每個核的指令都可能被亂序,另外,處理器還引入了L1,L2等緩存機制,每個核都有自己的緩存,這就導致了邏輯次序後寫入內存的未必真的最後寫入,最終導致了一個問題,如果我們不做任何防護措施,處理器最終得到的結果和邏輯得到的結果大不相同。

23、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 variable1、local variable2,這些本地變量仍然是存放在線程棧上的,即使這些方法所屬的對象存儲在堆上。
  5)、一個對象的成員變量可能會隨着這個對象自身存放在堆上,不管這個成員變量是原始類型還是引用類型。
  6)、靜態成員變量跟隨着類的定義一起存放在堆上,存放在堆上的對象可以被所持久對這個對象引用的線程訪問。
  7)、如果Thead存放了Object的引用,是可以訪問Object的,當一個線程可以訪問一個對象的時候,此線程也可以訪問這個對象的成員變量,當了兩個線程同時訪問一個對象的同一個方法,兩個線程會都訪問該對象的成員變量,但是每個線程都擁有該對象成員變量的私有拷貝。如Thead Stack同時調用了Object3對象的methodOne()方法。

24、計算機硬件架構簡單的圖示。如下所示:


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需要將結果回寫到主存的時候,它會將內部寄存器裏面的值刷新到緩存中,然後在某個時間點將值刷新到主存中。

25、Java內存模型與硬件內存架構之間的一些關聯。

  Java內存模型與硬件內存架構是存在一些差異的,硬件內存架構是沒有區分線程棧、堆、堆。對於硬件內存架構所有線程棧、堆都分佈在主內存中,部分線程棧和堆可能會出現在CPU緩存中,和CPU內部的寄存器裏面。

26、Java內存模型抽象結構圖。

  線程和主內存之間的抽象關係。線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存,本地內存是Java內存模型的一個抽象概念,並不是真實存在的,它涵蓋了緩存、寫緩衝區、寄存器以及其他的硬件和編譯器的優化。本地內存中存儲了該線程以讀或者寫共享變量拷貝的一個副本,比如如果線程A要使用主內存中共享變量,先拷貝主內存中一個共享變量副本,放到自己的本地內存中,從耕地的層次來說,主內存就是硬件的內存,是爲了獲取更好的運行速度,虛擬機和硬件內存可能會讓工作內存優先存儲於寄存器和高速緩存中。Java內存模型中的線程中的工作內存是CPU的寄存器和高速緩存的一個抽象的描述,而JVM的靜態存儲模型(即JVM內存模型)只是一種對內存的物理劃分而已,只侷限於內存,而且只侷限於JVM的內存。現在線程之間通信必須要經過主內存,如果線程A和線程B之間要進行通信,那麼必須經過兩個步驟,第一步,線程A將本地的內存A中的更新過的共享變量刷新到主內存中去,線程B去主內存中讀取線程A已經更新過的共享變量。

27、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操作)。

28、併發的優勢與風險。

 

作者:別先生

博客園:https://www.cnblogs.com/biehongli/

如果您想及時得到個人撰寫文章以及著作的消息推送,可以掃描上方二維碼,關注個人公衆號哦。

 

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