JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 一
| 版本 | 作者 | 內(nèi)容 |
|---|---|---|
| 2018.5.6 | chuIllusions | 首次發(fā)布 |
| 2018.5.29 | chuIllusions | 修改部分內(nèi)容 |
學(xué)習(xí)筆記說(shuō)明
本系列文章,是通過(guò)學(xué)習(xí)慕課網(wǎng)中Java并發(fā)編程與高并發(fā)解決方案整理的筆記,對(duì)課程的知識(shí)點(diǎn)進(jìn)行補(bǔ)充。同時(shí),感謝該課程老師所帶來(lái)的學(xué)習(xí)內(nèi)容,讓我深入學(xué)習(xí)了好多知識(shí)點(diǎn)。
相關(guān)文章
JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 二 之 線程安全性、安全發(fā)布對(duì)象
JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 三 之 線程安全策略
JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 四 之 J.U.C之AQS
JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 五 之 J.U.C組件拓展
JAVA并發(fā)編程與高并發(fā)解決方案 - 并發(fā)編程 六 之 線程池
學(xué)習(xí)內(nèi)容簡(jiǎn)介
并發(fā)編程知識(shí)點(diǎn)
線程安全、線程封閉、線程調(diào)度、同步容器、并發(fā)容器、AQS、J.U.C etc.
高并發(fā)解決方案知識(shí)點(diǎn)
擴(kuò)容、緩存、隊(duì)列、拆分、服務(wù)降級(jí)與熔斷、數(shù)據(jù)庫(kù)切庫(kù)、分庫(kù)分表 etc.
面對(duì)人群
從事JAVA開發(fā)的程序員
- 對(duì)并發(fā)和高并發(fā)不了解的程序員
- 對(duì)并發(fā)和高并發(fā)了解的程序員
- 已經(jīng)是編程高手的程序員
目的
構(gòu)建完整的并發(fā)與高并發(fā)知識(shí)體系
- 系統(tǒng)的學(xué)習(xí)到并發(fā)編程的知識(shí)及高并發(fā)處理思路
- 修正之前在不知不覺中犯過(guò)的一些并發(fā)方面的問(wèn)題
- 規(guī)避以后開發(fā)中一些并發(fā)方面的問(wèn)題
- 對(duì)你的知識(shí)進(jìn)行一次更為全面的梳理,完善知識(shí)體系
- 學(xué)習(xí)到大量的實(shí)際場(chǎng)景案例分析和代碼優(yōu)化技巧
- 讓你對(duì)并發(fā)編程和高并發(fā)處理有一個(gè)質(zhì)的提升
- 將節(jié)省你準(zhǔn)備面試的時(shí)間,讓你的面試更有針對(duì)性
- 可以借鑒一些之前可能沒有想到過(guò)的解決問(wèn)題思路和手段
課程內(nèi)容安排
基礎(chǔ)知識(shí)講解與核心知識(shí)準(zhǔn)備

并發(fā)及并發(fā)的線程安全處理

高并發(fā)處理的思路及手段

涉及知識(shí)技能
總體架構(gòu):Spring Boot、Maven、JDK8、MySQL
基礎(chǔ)組件:Mybatis、Guava、Lombok、Redis、Kafka
高級(jí)組件(類):Joda-Time、Atomic包、J.U.C、AQS、ThreadLocal、RateLimiter、Hystrix、ThreadPool、shardbatis、curator、elastic-job ...
場(chǎng)景舉例 - 實(shí)現(xiàn)計(jì)數(shù)功能
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class CountExample1 {
// 請(qǐng)求總數(shù)
public static int clientTotal = 5000;
// 同時(shí)并發(fā)執(zhí)行的線程數(shù)
public static int threadTotal = 200;
//5000個(gè)請(qǐng)求,每次只允許200個(gè)請(qǐng)求處理
public static int count = 0;
public static void main(String[] args) throws Exception {
//線程池 + 信號(hào)量 進(jìn)行請(qǐng)求的模擬
//新建線程池
ExecutorService executorService = Executors.newCachedThreadPool();
//定義信號(hào)量(后面會(huì)進(jìn)行講解)
final Semaphore semaphore = new Semaphore(threadTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
});
}
executorService.shutdown();
log.info("count:{}", count);
}
private static void add() {
count++;
}
}
點(diǎn)擊運(yùn)行結(jié)果,每一次結(jié)果都是不一樣的,并且沒有達(dá)到結(jié)果為5000,而是小于5000
@Slf4j
public class HashMapExample {
// 請(qǐng)求總數(shù)
public static int clientTotal = 5000;
// 同時(shí)并發(fā)執(zhí)行的線程數(shù)
public static int threadTotal = 200;
private static Map<Integer, Integer> map = new HashMap<>();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
for (int i = 0; i < clientTotal; i++) {
final int count = i;
executorService.execute(() -> {
try {
//每次允許threadTotal請(qǐng)求進(jìn)行處理
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
});
}
executorService.shutdown();
log.info("size:{}", map.size());
}
private static void update(int i) {
map.put(i, i);
}
}
點(diǎn)擊運(yùn)行結(jié)果,每一次結(jié)果都是不一樣的,并且Map.size()沒有達(dá)到結(jié)果為5000,而是小于5000
若上面兩個(gè)例子中,threadTotal = 1 則會(huì)得到我們預(yù)期的結(jié)果,size() = clientTotal = 5000
總結(jié):當(dāng)一個(gè)線程運(yùn)行可以得到我們預(yù)期的結(jié)果,但當(dāng)多個(gè)線程同時(shí)進(jìn)行操作,就會(huì)出現(xiàn)并發(fā)問(wèn)題,導(dǎo)致結(jié)果異常
@Slf4j
slf4j
對(duì)于一個(gè)maven項(xiàng)目。首先要在pom.xml中加入以下依賴項(xiàng):
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
- slf4j就是眾多接口的集合,它不負(fù)責(zé)具體的日志實(shí)現(xiàn),只在編譯時(shí)負(fù)責(zé)尋找合適的日志系統(tǒng)進(jìn)行綁定。具體有哪些接口,全部都定義在slf4j-api中。
- slf4j-log4j12是鏈接slf4j-api和log4j中間的適配器。它實(shí)現(xiàn)了slf4j-apiz中StaticLoggerBinder接口,從而使得在編譯時(shí)綁定的是slf4j-log4j12的getSingleton()方法
- log4j是具體的日志系統(tǒng)。通過(guò)slf4j-log4j12初始化Log4j,達(dá)到最終日志的輸出。
- lombok:一個(gè)插件,封裝了log的get和set,可以直接使用log來(lái)輸出日志信息。
@slf4j
如果不想每次都寫private final Logger logger = LoggerFactory.getLogger(XXX.class); 可以用注解@Slf4j
引入依賴,使用方式如上場(chǎng)景舉例中代碼示例
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
解決IDEA使用@Slf4j注入后找不到變量log
方式一:
idea中File --> settings --> Plugins --> 點(diǎn)擊"Browse repositories" --> 搜索lombok --> Install Lombok Plugins
若插件安裝失敗,則可以進(jìn)行以下安裝方式
方式二:
去idea官網(wǎng)下載插件 Lombok Plugin ,到下載區(qū),選擇合適的版本下載,我的idea版本為2017.1.4,因此選擇插件的版本號(hào)為0.16-2017.1.4 下載到文件為lombok-plugin-0.16.zip
注:idea任何插件的版本都需要跟idea版本對(duì)應(yīng),否則會(huì)提示安裝失敗(本人踩過(guò)的坑)
安裝步驟:解壓下載到的zip文件,拷貝解壓文件到idea安裝目錄下的plugins文件下,打開idea中的 plugins > 選擇 install plugin from disk > 選擇剛剛拷貝進(jìn)去的文件夾中的jar,即可進(jìn)行安裝,安裝完成后需要進(jìn)行重啟。
Lombox
簡(jiǎn)介:
Lombok項(xiàng)目是一個(gè)java庫(kù),可以自動(dòng)插入到您的編輯器和構(gòu)建工具中,讓您的java變得更加精彩。切勿再次寫入另一個(gè)getter或equals方法。提前訪問(wèn)未來(lái)的Java功能val,等等。
除了官方介紹中,并不多相關(guān)文章,特意挑了一篇文章中相關(guān)內(nèi)容
lombok 提供了簡(jiǎn)單的注解的形式來(lái)幫助我們簡(jiǎn)化消除一些必須有但顯得很臃腫的 java 代碼。特別是相對(duì)于 POJO。
簡(jiǎn)單來(lái)說(shuō),比如我們新建了一個(gè)類,然后在其中寫了幾個(gè)字段,然后通常情況下我們需要手動(dòng)去建立getter和setter方法啊,構(gòu)造函數(shù)啊之類的,lombok的作用就是為了省去我們手動(dòng)創(chuàng)建這些代碼的麻煩,它能夠在我們編譯源碼的時(shí)候自動(dòng)幫我們生成這些方法。lombok能夠達(dá)到的效果就是在源碼中不需要寫一些通用的方法,但是在編譯生成的字節(jié)碼文件中會(huì)幫我們生成這些方法,這就是lombok的神奇作用。
雖然有人可能會(huì)說(shuō)IDE里面都自帶自動(dòng)生成這些方法的功能,但是使用lombok會(huì)使你的代碼看起來(lái)更加簡(jiǎn)潔,寫起來(lái)也更加方便。
常用的注解
@slf4j、@Setter、@Getter、@NoArgsConstructor(注解在類上:為類提供一個(gè)無(wú)參的構(gòu)造方法)、@AllArgsConstructor(注解在類上;為類提供一個(gè)全參的構(gòu)造方法)
@NoArgsConstructor //注解在類上:為類提供一個(gè)無(wú)參的構(gòu)造方法
@AllArgsConstructor//注解在類上;為類提供一個(gè)全參的構(gòu)造方法
public class Person {
//@Getter @Setter 注解在屬性上;為屬性提供 setting 方法 getting方法
@Setter @Getter private int pid;
@Setter @Getter private String pname;
@Setter @Getter private int sage;
}
基礎(chǔ)知識(shí)講解與核心知識(shí)準(zhǔn)備
并發(fā)與高并發(fā)基本概念
概念
??并發(fā):同時(shí)擁有兩個(gè)或者多個(gè)線程,如果程序在單核處理器運(yùn)行,多個(gè)線程將交替地?fù)Q入或者換出內(nèi)存,這些線程是同時(shí)"存在"的,每個(gè)線程都處于執(zhí)行過(guò)程中的某個(gè)狀態(tài),如果運(yùn)行在多核處理器上,此時(shí),程序中的每個(gè)線程都將會(huì)分配到一個(gè)處理器核上,因此可以同時(shí)運(yùn)行
??并行:系統(tǒng)中有多個(gè)任務(wù)同時(shí)存在可稱之為“并發(fā)”,系統(tǒng)內(nèi)有多個(gè)任務(wù)同時(shí)執(zhí)行可稱之為“并行”;并發(fā)是并行的子集。如果說(shuō)并發(fā)就是在一臺(tái)處理器上"同時(shí)"處理多個(gè)任務(wù),那么并行就是在多臺(tái)處理器上同時(shí)處理多個(gè)任務(wù);個(gè)人理解是,在單核CPU系統(tǒng)上,并行是無(wú)法實(shí)現(xiàn)的,只可能存在并發(fā)而不可能存在并行。
??高并發(fā):高并發(fā)(High Concurrency)是互聯(lián)網(wǎng)分布式系統(tǒng)架構(gòu)設(shè)計(jì)中必須考慮的因素之一,它通常指,通過(guò)設(shè)計(jì)保證系統(tǒng)能夠同時(shí)并行處理很多請(qǐng)求。
對(duì)比:
??并發(fā):多個(gè)線程操作相同的資源,保證線程安全,合理使用資源
??高并發(fā):服務(wù)能同時(shí)處理很多請(qǐng)求,提高程序性能;如系統(tǒng)集中收到大量的請(qǐng)求(12306的搶票系統(tǒng)),導(dǎo)致系統(tǒng)在某段時(shí)間類執(zhí)行大量的操作,包括對(duì)資源的請(qǐng)求、數(shù)據(jù)庫(kù)的操作等等,如果高并發(fā)處理不好,不僅僅降低用戶的體驗(yàn)度,請(qǐng)求時(shí)間變長(zhǎng),同時(shí)也可能導(dǎo)致系統(tǒng)宕機(jī),甚至導(dǎo)致OOM(Out Of Memory)異常,如果想要系統(tǒng)適應(yīng)高并發(fā)狀態(tài),就要有多個(gè)方面進(jìn)行系統(tǒng)優(yōu)化,包括硬件、網(wǎng)絡(luò)、系統(tǒng)架構(gòu)、開發(fā)語(yǔ)言的選取、數(shù)據(jù)結(jié)構(gòu)的應(yīng)用、算法的優(yōu)化等等,這個(gè)時(shí)候談?wù)摰氖侨绾翁峁┈F(xiàn)有程序的性能,對(duì)高并發(fā)場(chǎng)景提供一些解決方案、手段等等
CPU多級(jí)緩存
??在多線程并發(fā)環(huán)境下,如果不采取特殊手段,普通的累加結(jié)果很可能是錯(cuò)的。錯(cuò)的原因可能涉及到計(jì)算機(jī)原理以及JAVA方面的一些知識(shí)。
介紹

Main Memory : 主存
Cache : 高速緩存,數(shù)據(jù)的讀取和存儲(chǔ)都經(jīng)過(guò)此高速緩存
CPU Core : CPU核心
Bus : 系統(tǒng)總線
??CUP Core 與 Cache 之間有一條快速通道,Main Memory 與 Cache 關(guān)聯(lián)在 Bus 上,同時(shí) Bus 還用于其他組件 的通信,在Cache出現(xiàn)不久后,系統(tǒng)變得更加復(fù)雜,Cache與Main Memory中速度的差異拉大,直到加入另一級(jí)的Cache,新加入的Cache 比 一級(jí) Cache 更大,但是更慢,由于從加大一級(jí)Cache的做法,從經(jīng)濟(jì)上是行不通的,所以有了二級(jí)Cache,甚至已經(jīng)有三級(jí) Cache
為什么需要CPU CACHE?
??CPU的頻率太快了,快到主存跟不上,這樣在處理器時(shí)鐘周期內(nèi),CPU常常需要等待主存,浪費(fèi)資源,這樣會(huì)使CPU花費(fèi)很長(zhǎng)時(shí)間等待數(shù)據(jù)到來(lái)或把數(shù)據(jù)寫入內(nèi)存。所以Cache的出現(xiàn),是為了緩解CPU和內(nèi)存之間速度的不匹配問(wèn)題(結(jié)構(gòu):CPU - > CACHE - > MEMORY)
CPU CACHE 意義
??緩存的容量遠(yuǎn)遠(yuǎn)小于主存,因此出現(xiàn)緩存不命中的情況在所難免,既然緩存不能包含CPU所需要的所有數(shù)據(jù),那么Cache的存在真的有意義嗎?
CPU緩存存在的意義分兩點(diǎn)(局部性原理):
- 時(shí)間局部性:如果某個(gè)數(shù)據(jù)被訪問(wèn),那么在不久的將來(lái)它很可能被再次訪問(wèn)
- 空間局限性:如果某個(gè)數(shù)據(jù)被訪問(wèn),那么與它相鄰的數(shù)據(jù)很快也可能被訪問(wèn)
??緩存的工作原理是當(dāng)CPU要讀取一個(gè)數(shù)據(jù)時(shí),首先從緩存中查找,如果找到就立即讀取并運(yùn)送給CPU處理;如果沒有找到,就用相對(duì)慢的速度內(nèi)存中讀取并運(yùn)送給CPU處理,同時(shí)把這個(gè)數(shù)據(jù)所在的數(shù)據(jù)塊調(diào)入緩存中,可以使得以后對(duì)整塊數(shù)據(jù)的讀取都從緩存中進(jìn)行,不必再調(diào)用內(nèi)存。
??正是這樣的讀取機(jī)制使CPU讀取緩存的命中率非常高(大多數(shù)CPU可達(dá)90%左右),也就是說(shuō)CPU下一次要讀取的數(shù)據(jù)90%都在緩存中,大約10%需要從內(nèi)存讀取。
緩存一致性(MESI)
??緩存一致性用于保證多個(gè)CPU Cache之間緩存共享數(shù)據(jù)的一致性,定義了Cache Line四種狀態(tài),而CPU對(duì)Cache的四種操作,可能會(huì)產(chǎn)生不一致的狀態(tài),因此緩存控制器監(jiān)聽到本地操作和遠(yuǎn)程操作的時(shí)候 ,需要對(duì)Cache Line作出相應(yīng)的修改,從而保證數(shù)據(jù)在多個(gè)緩存之間的一致性
??Cache Line : 是cache與內(nèi)存數(shù)據(jù)交換的最小單位,根據(jù)操作系統(tǒng)一般是32byte或64byte。在MESI協(xié)議中,狀態(tài)可以是M、E、S、I,地址則是cache line中映射的內(nèi)存地址,數(shù)據(jù)則是從內(nèi)存中讀取的數(shù)據(jù)。
??MESI其實(shí)是四種狀態(tài)的縮寫:M(modify)修改、E(exclusive)獨(dú)占、S(shared)共享、I(invalid)失效。
狀態(tài)間的相互轉(zhuǎn)換關(guān)系:
| M | E | S | I | |
|---|---|---|---|---|
| M | × | × | × | √ |
| E | × | × | × | √ |
| S | × | × | √ | √ |
| I | √ | √ | √ | √ |
??Cache 操作: MESI協(xié)議中,每個(gè)cache的控制器不僅知道自己的操作(local read和local write),通過(guò)監(jiān)聽也知道其他CPU中cache的操作(remote read和remote write)。對(duì)于自己本地緩存有的數(shù)據(jù),CPU僅需要發(fā)起local操作,否則發(fā)起remote操作,從主存中讀取數(shù)據(jù),cache控制器通過(guò)總線監(jiān)聽,僅能夠知道其他CPU發(fā)起的remote操作,但是如果local操作會(huì)導(dǎo)致數(shù)據(jù)不一致性,cache控制器會(huì)通知其他CPU的cache控制器修改狀態(tài)。
亂序執(zhí)行優(yōu)化
??處理器為提高運(yùn)算速度而做出違背代碼原有順序的優(yōu)化
舉個(gè)例子:
- 計(jì)算 a * b ,a =10 ,b = 200 ,則 result = a * b = 2000
- 代碼編寫順序:a=10 -> b=200 -> result = a * b
- CPU亂序執(zhí)行優(yōu)化可能會(huì)發(fā)生執(zhí)行順序?yàn)椋篵=200 -> a=10 -> result = a * b
??CPU亂序執(zhí)行優(yōu)化不會(huì)對(duì)結(jié)果造成影響,在單核時(shí)代,處理器保證做出的優(yōu)化,不會(huì)導(dǎo)致執(zhí)行的結(jié)果遠(yuǎn)離預(yù)期的目標(biāo),但是在多核環(huán)境下并非如此。首先在多核環(huán)境中,同時(shí)會(huì)有多個(gè)核執(zhí)行指令,每個(gè)核的指定都可能會(huì)被亂序優(yōu)化,另外,處理器還引用了L1、L2等緩存機(jī)制,每個(gè)核都有自己的緩存,這就導(dǎo)致了邏輯次序上后寫入內(nèi)存的數(shù)據(jù),未必真的最后寫入,最終帶來(lái)了這樣的一個(gè)問(wèn)題:如果我們不做任何防護(hù)措施,處理器最終得到的結(jié)果和我們邏輯得出的結(jié)果大不相同。比如我們?cè)谄渲械囊粋€(gè)核中執(zhí)行數(shù)據(jù)寫入操作,并在最后寫一個(gè)標(biāo)記,用來(lái)標(biāo)記數(shù)據(jù)已經(jīng)準(zhǔn)備好了,然后從另外一個(gè)核上,通過(guò)那個(gè)標(biāo)志,來(lái)判斷數(shù)據(jù)是否已經(jīng)就緒,這種做法它就存在一定的風(fēng)險(xiǎn),標(biāo)記位先被寫入,但數(shù)據(jù)操作并未完成(可能是計(jì)算為完成、也可能是數(shù)據(jù)沒有從緩存刷新到主存當(dāng)中), 最終導(dǎo)致另外的核使用了錯(cuò)誤的數(shù)據(jù)。
Java 內(nèi)存模型(Java Memory Model,JMM)
??CPU緩存一致性和亂序執(zhí)行優(yōu)化,在多核多并發(fā)下,需要額外做很多的事情,才能保證程序的執(zhí)行,符合我們的預(yù)期。那么JVM(Java Virtual Machine (Java虛擬機(jī)))是如何解決這些問(wèn)題的?為了屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的并發(fā)效果,JMV規(guī)范中定義了JMM (Java Memory Model (Java 內(nèi)存模型))。 JMM是一種規(guī)范,它規(guī)范了JVM與計(jì)算機(jī)內(nèi)存是如何協(xié)同工作的,它規(guī)定一個(gè)線程如何和何時(shí)可以看到其他線程修改過(guò)的共享變量的值,以及在必須時(shí)如何同步的訪問(wèn)共享變量。
JVM內(nèi)存分配概念

JVM內(nèi)存分配的兩個(gè)概念:Stack(棧)和Heap(堆)。
??Java中的Heap是運(yùn)行時(shí)數(shù)據(jù)區(qū),由垃圾回收負(fù)責(zé),它的優(yōu)勢(shì)是動(dòng)態(tài)的分配內(nèi)存大小,生存期也不必事先告訴編譯器,在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存,Java的垃圾收集器,會(huì)自動(dòng)回收不再使用的數(shù)據(jù)。但是也有缺點(diǎn),由于是要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存,因此存取速度相對(duì)較慢。
??Java中的Stack優(yōu)勢(shì)是存取速度比Heap要快,僅次于計(jì)算機(jī)中的寄存器,棧中的數(shù)據(jù)是可以共享的,但是它的缺點(diǎn)是,存在棧中數(shù)據(jù)的大小和生存期必須是確定的,缺乏靈活性,主要存放一些基本類型的變量。
??JMM要求調(diào)用棧和本地變量存放在線程棧中,對(duì)象存放在堆上。一個(gè)本地變量可能指向一個(gè)對(duì)象的引用,引用這個(gè)本地變量是存放在線程棧上,而對(duì)象本身是存放在堆上的。一個(gè)對(duì)象可能包含方法,這些方法可能包含本地變量,這些本地變量還是存放在線程棧中,即使這些方法所屬的對(duì)象存放在堆上。一個(gè)對(duì)象的成員變量可能會(huì)隨著這個(gè)對(duì)象自身存放在堆上,不管這個(gè)成員對(duì)象是原始類型還是引用類型,靜態(tài)成員變量跟隨著類的定義一起存放在堆上。存放在堆上的對(duì)象,可以被所持有對(duì)這個(gè)對(duì)象引用線程的訪問(wèn)。
??當(dāng)一個(gè)線程可以訪問(wèn)一個(gè)對(duì)象的時(shí)候,它也可以訪問(wèn)該對(duì)象的成員變量,如果兩個(gè)線程同時(shí)調(diào)用同一個(gè)對(duì)象的同一個(gè)方法,將會(huì)都訪問(wèn)該對(duì)象的成員變量,但是每一個(gè)線程都擁有了這個(gè)成員變量的私有拷貝。
計(jì)算機(jī)內(nèi)存硬件架構(gòu)

??CPU,一臺(tái)現(xiàn)代計(jì)算機(jī)擁有兩個(gè)或多個(gè)CPU,其中一些CPU還有多核,從這一點(diǎn)可以看出,在一個(gè)有兩個(gè)或多個(gè)CPU的現(xiàn)代計(jì)算機(jī)上,同時(shí)運(yùn)行多個(gè)線程是非常有可能的,而且每個(gè)CPU在某一個(gè)時(shí)刻,運(yùn)行一個(gè)線程是肯定沒有問(wèn)題的,這意味著,如果Java程序是多線程的,在Java程序中,每個(gè)CPU上一個(gè)線程是可能同時(shí)并發(fā)執(zhí)行的。
??CPU Refisters(寄存器),每個(gè)CPU都包含一系列的寄存器,它們是CPU內(nèi)存的基礎(chǔ),CPU在寄存器中執(zhí)行操作的速度遠(yuǎn)大于在主存上執(zhí)行的速度,這是因?yàn)镃PU訪問(wèn)寄存器的速度遠(yuǎn)大于主存。
??Cache(高速緩存),由于計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器運(yùn)算速度之間有著幾個(gè)數(shù)量級(jí)的差距,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運(yùn)算速度的高級(jí)緩存來(lái)作為內(nèi)存與處理器之間的緩沖,將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速的進(jìn)行,當(dāng)運(yùn)算結(jié)束后,在從緩存同步到內(nèi)存中。這樣處理器就無(wú)需等待緩慢的內(nèi)存讀寫,CPU訪問(wèn)緩存層的速度快于訪問(wèn)主存的速度,但通常比訪問(wèn)內(nèi)部寄存器的速度要慢。
??Main Memory(主存),隨機(jī)存取存儲(chǔ)器(random access memory,RAM)又稱作“隨機(jī)存儲(chǔ)器",一個(gè)計(jì)算機(jī)包含一個(gè)主存,所有的CPU都可以訪問(wèn)主存,主存通常比CPU中的緩存大得多。
JVM 與 Computer

? JVM 與 Computer 內(nèi)存架構(gòu)存在差異,硬件內(nèi)存并無(wú)區(qū)分棧與堆,對(duì)于硬件而言,所有的棧和堆都分布在主內(nèi)存中,可能會(huì)出現(xiàn)在高速緩存、寄存器中。
內(nèi)存模型抽象結(jié)構(gòu)

Java內(nèi)存模型 - 同步八種操作
- lock(鎖定):作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)
- unlock(解鎖):作用于主內(nèi)存的變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定
- read(讀?。鹤饔糜谥鲀?nèi)存的變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用
- load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值存放工作內(nèi)存的變量副本中
- use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎
- assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量
- store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳遞到主內(nèi)存中,以便隨后的write的操作
- write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存的變量中
Java內(nèi)存模型 - 同步規(guī)則
- 如果要把一個(gè)變量從主內(nèi)存中復(fù)制到工作內(nèi)存,就需要按順序地執(zhí)行read和load操作,如果把變量從工作內(nèi)存中同步回主內(nèi)存中,就要按順序地執(zhí)行store和write操作,但Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒有保證是連續(xù)執(zhí)行
- 不允許read和load、store和write操作之一單獨(dú)出現(xiàn)
- 不允許一個(gè)線程丟棄它的最近assign的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中
- 不允許一個(gè)線程無(wú)原因地(沒有發(fā)生過(guò)任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中
- 一個(gè)新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量。即就是對(duì)一個(gè)變量實(shí)施use和store操作之前,必須先執(zhí)行過(guò)了assign和load操作
- 一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次與執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock,變量才會(huì)被解鎖。lock和unlock必須成對(duì)出現(xiàn)
- 如果一個(gè)變量執(zhí)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
- 如果一個(gè)變量事先沒有被lock操作鎖定,則不允許對(duì)它執(zhí)行unlock操作;也不允許去unlock一個(gè)被其他線程鎖定的變量
- 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把變量同步到主內(nèi)存中(執(zhí)行store和write操作)
Java 內(nèi)存模型 - 同步操作與規(guī)則

并發(fā)的優(yōu)勢(shì)與風(fēng)險(xiǎn)

并發(fā)編程與線程安全
??代碼所在的進(jìn)程,有多個(gè)線程同時(shí)運(yùn)行,而這些線程可能會(huì)同時(shí)運(yùn)行同一段代碼,如果每次運(yùn)行結(jié)果和單線程預(yù)期結(jié)果一致,變量值也和預(yù)期一致,則認(rèn)為這是線程安全的。簡(jiǎn)單的說(shuō),就是并發(fā)環(huán)境下,得到我們期望正確的結(jié)果。對(duì)應(yīng)的一個(gè)概念就是線程不安全,就是不提供數(shù)據(jù)訪問(wèn)保護(hù),有可能出現(xiàn)多個(gè)線程,先后更改數(shù)據(jù),造成所得到的數(shù)據(jù)是臟數(shù)據(jù),也可能是計(jì)算錯(cuò)誤。
環(huán)境搭建準(zhǔn)備
項(xiàng)目架構(gòu)
Spring Boot 項(xiàng)目,https://start.spring.io
自定義注解
? 為方便理解,自定義一些注解,方便理解。
/**
* 課程里用來(lái)標(biāo)記【線程安全】的類或者寫法
*/
@Target(ElementType.TYPE) //作用域,作用于類上
@Retention(RetentionPolicy.SOURCE) //注解存在的范圍,編譯時(shí)忽略
public @interface ThreadSafe {
//給默認(rèn)值,方便擴(kuò)展
String value() default "";
}
/**
* 課程里用來(lái)標(biāo)記【線程不安全】的類或者寫法
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface NotThreadSafe {
String value() default "";
}
/**
* 課程里用來(lái)標(biāo)記【推薦】的類或者寫法
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Recommend {
String value() default "";
}
/**
* 課程里用來(lái)標(biāo)記【不推薦】的類或者寫法
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface NotRecommend {
String value() default "";
}
并發(fā)模擬
模擬準(zhǔn)備工具介紹
- Postman:Http請(qǐng)求模擬工具
- Apache Bench(AB):Apache附帶的工具,測(cè)試網(wǎng)站性能
- JMeter:Apache組織開發(fā)的壓力測(cè)試工具
- 代碼模擬:Semaphore、CountDownLatch等
服務(wù)準(zhǔn)備
@RequestMapping("/test")
@ResponseBody
public String test() {
return "test";
}
Postman
? Postman本身是一個(gè)Http模擬工具,在并發(fā)上并不是專業(yè)的
使用步驟:
- 打開Postman訪問(wèn)
localhost:8080/test,完成一次服務(wù)訪問(wèn) - 找到Collections標(biāo)簽,新建concurrency文件夾,將剛訪問(wèn)的連接Save文件夾中,并點(diǎn)擊文件夾進(jìn)入測(cè)試準(zhǔn)備
- 配置參數(shù),點(diǎn)擊Run Concurrency,成功后查看結(jié)果



Apache Bench(AB)
??Apache Bench 是 Apache 服務(wù)器自帶的一個(gè)web壓力測(cè)試工具,簡(jiǎn)稱ab。ab又是一個(gè)命令行工具,對(duì)發(fā)起負(fù)載的本機(jī)要求很低,根據(jù)ab命令可以創(chuàng)建很多的并發(fā)訪問(wèn)線程,模擬多個(gè)訪問(wèn)者同時(shí)對(duì)某一URL地址進(jìn)行訪問(wèn),因此可以用來(lái)測(cè)試目標(biāo)服務(wù)器的負(fù)載壓力??偟膩?lái)說(shuō)ab工具小巧簡(jiǎn)單,上手學(xué)習(xí)較快,可以提供需要的基本性能指標(biāo),但是沒有圖形化結(jié)果,不能監(jiān)控。
Windows 7 安裝
- 首先需要安裝Apache服務(wù)器,點(diǎn)擊下載
- 將下載
httpd-2.4.33-win64-VC15.zip解壓 - 配置環(huán)境變量,這里為了方便,我沒有配置,直接進(jìn)入bin目錄,運(yùn)行控制臺(tái)
- 輸入ab命名,若出現(xiàn)以下提示則環(huán)境準(zhǔn)備成功
D:\apache\Apache24\bin>ab
ab: wrong number of arguments
Usage: ab [options] [http://]hostname[:port]/path
Options are:
-n requests Number of requests to perform
-c concurrency Number of multiple requests to make at a time
-t timelimit Seconds to max. to spend on benchmarking
This implies -n 50000
-s timeout Seconds to max. wait for each response
Default is 30 seconds
-b windowsize Size of TCP send/receive buffer, in bytes
-B address Address to bind to when making outgoing connections
-p postfile File containing data to POST. Remember also to set -T
-u putfile File containing data to PUT. Remember also to set -T
-T content-type Content-type header to use for POST/PUT data, eg.
'application/x-www-form-urlencoded'
Default is 'text/plain'
-v verbosity How much troubleshooting info to print
-w Print out results in HTML tables
-i Use HEAD instead of GET
-x attributes String to insert as table attributes
-y attributes String to insert as tr attributes
-z attributes String to insert as td or th attributes
-C attribute Add cookie, eg. 'Apache=1234'. (repeatable)
-H attribute Add Arbitrary header line, eg. 'Accept-Encoding: gzip'
Inserted after all normal header lines. (repeatable)
-A attribute Add Basic WWW Authentication, the attributes
are a colon separated username and password.
-P attribute Add Basic Proxy Authentication, the attributes
are a colon separated username and password.
-X proxy:port Proxyserver and port number to use
-V Print version number and exit
-k Use HTTP KeepAlive feature
-d Do not show percentiles served table.
-S Do not show confidence estimators and warnings.
-q Do not show progress when doing more than 150 requests
-l Accept variable document length (use this for dynamic pages)
-g filename Output collected data to gnuplot format file.
-e filename Output CSV file with percentages served
-r Don't exit on socket receive errors.
-m method Method name
-h Display usage information (this message)
提示:若啟動(dòng)ab.exe時(shí)候,提示缺少某種依賴庫(kù),則需要安裝該依賴庫(kù)才可進(jìn)行啟動(dòng)
運(yùn)行演示
運(yùn)行命令:ab -n 1000 -c 50 http://localhost:8080/test
命令解析:-n 請(qǐng)求總次數(shù) -c 并發(fā)數(shù) URL地址
D:\apache\Apache24\bin>ab -n 1000 -c 50 http://localhost:8080/test
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software:
Server Hostname: localhost
Server Port: 8080
Document Path: /test
Document Length: 4 bytes
Concurrency Level: 50 # 并發(fā)量
Time taken for tests: 0.834 seconds # 整個(gè)測(cè)試使用的時(shí)間
Complete requests: 1000 # 完成請(qǐng)求數(shù)
Failed requests: 0 # 失敗請(qǐng)求數(shù)
Total transferred: 136000 bytes # 所有請(qǐng)求響應(yīng)數(shù)據(jù)的總和(包括Http 頭信息和正文數(shù)據(jù)長(zhǎng)度,服務(wù)器流向應(yīng)用層數(shù)據(jù)總長(zhǎng)度)
HTML transferred: 4000 bytes # 所有響應(yīng)數(shù)據(jù),正文數(shù)據(jù)總和
Requests per second: 1198.97 [#/sec] (mean) # 吞吐率,與并發(fā)數(shù)相關(guān)
Time per request: 41.702 [ms] (mean) # 用戶平均請(qǐng)求等待時(shí)間
Time per request: 0.834 [ms] (mean, across all concurrent requests) # 服務(wù)器平均請(qǐng)求等待時(shí)間
Transfer rate: 159.24 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.8 0 24
Processing: 1 39 87.7 14 584
Waiting: 0 31 75.8 11 558
Total: 2 39 87.7 14 584
Percentage of the requests served within a certain time (ms)
50% 14
66% 20
75% 27
80% 31
90% 50
95% 220
98% 395
99% 464
100% 584 (longest request)
JMeter
??相對(duì)于AB來(lái)說(shuō),JMeter更加強(qiáng)大。Apache JMeter是Apache組織開發(fā)的基于Java的壓力測(cè)試工具。JMeter 可以用于對(duì)服務(wù)器、網(wǎng)絡(luò)或?qū)ο竽M巨大的負(fù)載,來(lái)自不同壓力類別下測(cè)試它們的強(qiáng)度和分析整體性能。
參考文章:JMeter使用入門
Windows 7 安裝
- 進(jìn)入官網(wǎng)下載
- 將下載后的
apache-jmeter-4.0.zip解壓 - 進(jìn)入解壓目錄中的bin目錄,運(yùn)行
jmeter.bat
運(yùn)行演示
創(chuàng)建線程組


Number of Threads(users) : 線程數(shù)、虛擬用戶數(shù)
Ramp-Up Period(in second) : 虛擬用戶增長(zhǎng)時(shí)長(zhǎng)。理解:假設(shè)現(xiàn)在有一個(gè)考勤系統(tǒng) ,所有的用戶都不是同時(shí)登陸的,實(shí)際使用場(chǎng)景是在某段時(shí)間內(nèi),用戶會(huì)陸陸續(xù)續(xù)的進(jìn)行考勤,而這個(gè)參數(shù)大概理解就是這個(gè)意思,考勤是從8點(diǎn)40分到9點(diǎn)10分,那么這個(gè)參數(shù)就是30分鐘*60秒,意味著指定用戶請(qǐng)求在規(guī)定時(shí)間內(nèi)完成請(qǐng)求。
Loop Count : 循環(huán)次數(shù),每個(gè)虛擬用戶循環(huán)的次數(shù),如果勾選Forever則會(huì)一直進(jìn)行下去,默認(rèn)是1
添加請(qǐng)求


為請(qǐng)求添加結(jié)果監(jiān)聽:圖形結(jié)果(Graph Results)與查看結(jié)果樹(View Results Tree)

結(jié)果分析


代碼模擬
CountDownLatch
??CountDownLatch類位于java.util.concurrent包下,利用它可以實(shí)現(xiàn)類似計(jì)數(shù)器的功能。比如有一個(gè)任務(wù)A,它要等待其他4個(gè)任務(wù)執(zhí)行完畢之后才能執(zhí)行,此時(shí)就可以利用CountDownLatch來(lái)實(shí)現(xiàn)這種功能了。

假設(shè)計(jì)數(shù)器的值為3,線程A調(diào)用await()方法之后,當(dāng)前線程就進(jìn)入了等待狀態(tài), 之后在其他線程中執(zhí)行countDown(),計(jì)數(shù)器就會(huì) - 1 ,該操作線程繼續(xù)執(zhí)行,當(dāng)計(jì)數(shù)器從3變成0之后,線程A繼續(xù)執(zhí)行。
CountDownLatch這個(gè)類可以阻塞線程,保證線程在某種特定的條件下,繼續(xù)執(zhí)行。
Semaphore

??Semaphore翻譯成字面意思為 信號(hào)量,Semaphore可以阻塞進(jìn)程并且控制同時(shí)訪問(wèn)的線程個(gè)數(shù),通過(guò) acquire() 獲取一個(gè)許可,如果沒有就等待,而 release() 釋放一個(gè)許可。Semaphore其實(shí)和鎖有點(diǎn)類似,它一般用于控制對(duì)某組資源的訪問(wèn)權(quán)限。
??CountDownLatch與Semaphore在使用時(shí),通常會(huì)與線程池配合使用
??Semaphore適合控制并發(fā)數(shù),CountDownLatch比較適合保證線程執(zhí)行完后再執(zhí)行其他處理,因此模擬并發(fā)時(shí),使用兩者結(jié)合起來(lái)是最好的。
并發(fā)模擬代碼實(shí)現(xiàn)
@Slf4j
@NotThreadSafe //線程不安全的
public class ConcurrencyTest {
// 請(qǐng)求總數(shù)
public static int clientTotal = 5000;
// 同時(shí)并發(fā)執(zhí)行的線程數(shù)
public static int threadTotal = 200;
public static int count = 0;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
//定義信號(hào)量
final Semaphore semaphore = new Semaphore(threadTotal);
//定義計(jì)數(shù)器閉鎖
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire(); //獲取信號(hào)量,否則會(huì)阻塞
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown(); //每執(zhí)行一次則減1
});
}
countDownLatch.await();
executorService.shutdown(); //關(guān)閉線程池
log.info("count:{}", count);
}
//線程不安全
private static void add() {
count++;
}
}