對高并發(fā)理解
高并發(fā)是一種高效解決問題的思維模式赋咽。上小學的時候,老師問過這樣一個問題:燒開水需要20分鐘吨娜,洗碗需要20分鐘脓匿,請問小明如何在30分鐘內做完兩件事。作為一個智力正常的成年人肯定很快就能想到方案宦赠,不過當時我是沒想到可以兩個事情同時做陪毡,所以現(xiàn)在還對這簡單問題耿耿于懷。
宏觀理解
背景
隨著計算機性能和互聯(lián)網的發(fā)展普及勾扭,軟件系統(tǒng)需要處理的業(yè)務復雜度和吞吐率有了巨大變化毡琉,為了應對需求的,軟件架構也一直發(fā)生著演化妙色。
早期的單體架構桅滋,一臺服務器存儲數(shù)據(jù),也做業(yè)務計算身辨,主要面向的企業(yè)應用和早期的網站丐谋。特點是使用人數(shù)不多,業(yè)務簡單煌珊。
近期的分布式多層微服務架構号俐,此架構通常是一個集群服務模式,可以對某個服務進行在線擴展定庵,集群規(guī)睦舳觯可達數(shù)萬臺践美。此架構主要面向大規(guī)模用戶訪問的互聯(lián)網應用。特點是并發(fā)量大找岖,業(yè)務伸縮性強。
宏觀層高并發(fā)思路
通過單體架構向分布式多層微服務的架構變化敛滋,可以看出在宏觀層面许布,高并發(fā)的解決方案分布式集群,因此解決集群組織問題是高并發(fā)的核心绎晃。在單體架構模式下相當于是一個人自產自銷的小農模式蜜唾,結構簡單,無需關注外部變化庶艾;演變到分布式多層微服務架構的時候相當于是一個超級跨國公司袁余,因此如何組織好各個部分,讓他們有序高效的協(xié)同工作就是核心的挑戰(zhàn)咱揍。
微觀理解
背景
并發(fā)是指一段時間內颖榜,有多個任務工作
并行是指同一時間點,有多個任務同時工作
并發(fā)和并行
并發(fā)和并行是兩個不同的概念煤裙。在單核CPU下是無法完成并行的掩完,但是可以通過CPU時鐘分片的方式完成并發(fā)。使用并發(fā)的根本原因在于CPU處理指令的速度遠大于IO(磁盤IO,網絡IO)的速度硼砰。
假設有一個對10GB數(shù)據(jù)的進行排序需求且蓬,CPU完成排序可能只需要1分鐘,但是這些數(shù)據(jù)從磁盤讀取到內存题翰,再到緩存可能需要20分鐘恶阴,如果不做并發(fā)處理,相當于19分鐘CPU是空閑的豹障,便會造成一種資源浪費冯事。總的來說并發(fā)會帶來三個好處:
更合理利用CPU資源血公,盡量減少CPU的空閑時間
優(yōu)化程序設計桅咆,不同任務由不同的線程處理,結構更加清晰
及時響應坞笙,并發(fā)結合異步等手段可實現(xiàn)及時相應用戶請求
微觀層高并發(fā)思路
微觀層高并發(fā)的核心需求是能夠提高CPU的利用率岩饼,或者說降低CPU速度(Vc)和IO速度(Ic)的比值(Vc/Ic)。通常任務層可以通過線程的方式實現(xiàn)多任務并發(fā)處理薛夜,IO層可以通過緩存的方式提高數(shù)據(jù)讀取速度籍茧。但多任務、緩存之間較之單任務會帶來緩存一致性等問題梯澜,也就是不同任務處理同一份數(shù)據(jù)寞冯,如何保證計算結果是預期的。通常并發(fā)任務為了保證預期結果需要確保:
原子性 即一個或者多個操作作為一個整體,要么全部執(zhí)行吮龄,要么都不執(zhí)行俭茧,并且操作在執(zhí)行過程中不會被線程調度機制打斷;而且這種操作一旦開始漓帚,就一直運行到結束母债,中間不會有任何上下文切換。
可見性 是指當多個任務訪問同一個內存變量時尝抖,一個線程修改了這個內存變量的值毡们,其他線程能夠立即看得到修改的值。
有序性 即任務執(zhí)行的順序按照任務定義流程的先后順序執(zhí)行
JAVA高并發(fā)編程
Java語言內置了多線程支持昧辽。Java程序是運行于JVM(Java 虛擬機)中的衙熔,一個Java程序實際上是一個JVM進程,進程中啟動了一個主線程來執(zhí)行main()
方法(Java 入口函數(shù))搅荞,在main()
方法中又可以啟動其他線程红氯。此外,JVM中也有工作線程咕痛,如垃圾回收脖隶。
對于Java程序來說,實際上就是處于多線程的編程環(huán)境中暇检。
JVM和JAVA內存模型(JMM)
JVM(Java Virtual Machine) 是一種基于計算設備的規(guī)范产阱,是一臺虛擬機,即虛構的計算機块仆。
JMM(Java Memory Model) 是一種規(guī)范了Java虛擬機與計算機內存是如何協(xié)同工作的協(xié)議构蹬。
JVM概要介紹
類加載器 加載Java字節(jié)碼文件徙赢,JVM入口
Java棧 本地方法棧疚顷、程序計算器躁染,Java線程運行存儲數(shù)據(jù)結構
方法區(qū) 存儲常量睛榄、類信息、接口信息脐嫂、方法信息等元數(shù)據(jù)
堆 存儲程序創(chuàng)建的對象梗掰,垃圾回收區(qū)域
執(zhí)行引擎 調用操作系統(tǒng)執(zhí)行指令的出口
例如下程序:
public class Jvm {
final int var1 = 0;
static int var2 = 1;
public void test(){
int localVar = 3;
System.out.println("test is run");
}
public static void main(String[] args){
Jvm jvm = new Jvm();
jvm.test();
}
}
Jvm.java通過javac 編譯成Jvm.class后由類加載器加載到JVM中爽雄,其中因為var1
头滔、var2
被static
怖亭、final
修飾,可以理解為常量坤检,同test()
方法等類元數(shù)據(jù)信息存儲于方法區(qū)兴猩。new Jvm()
出來的jvm
對象則存儲于堆內存中。此程序是運行于main
線程中早歇,因此有一個主線程Java棧內存倾芝,存儲localVar
這樣的局部變量讨勤、程序計算器、輸入輸出參數(shù)晨另、對象引用地址等潭千。執(zhí)行的過程通過執(zhí)行引擎調用系統(tǒng)接口執(zhí)行指令完成任務。最終運行完成后JVM進程完成垃圾回收借尿,退出進程刨晴。
JMM概要介紹
現(xiàn)代計算機內存模型
現(xiàn)代物理計算機大多數(shù)也是多核模型,任務能夠并行運行于不同核上垛玻。當不同任務對共享的數(shù)據(jù)進行讀寫的時候,需要和內存交互奶躯。為了降低CPU運輸速度和內存IO速度的大小帚桩,現(xiàn)代計算機系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存(Cache)來作為內存與處理器之間的緩沖:將運算需要使用到的數(shù)據(jù)復制到緩存中,讓運算能快速進行嘹黔,當運算結束后再從緩存同步回內存之中账嚎,這樣處理器就無須等待緩慢的內存讀寫了。但是提供速度的同時也引入了緩存一致性(Cache Coherence)問題儡蔓。為了解決一致性的問題郭蕉,需要各個處理器訪問緩存時都遵循一些協(xié)議,在讀寫時要根據(jù)協(xié)議來進行操作喂江,這類協(xié)議有MSI召锈、MESI(Illinois Protocol)、MOSI获询、Synapse涨岁、Firefly及Dragon Protocol等。
JMM
Java內存模型中規(guī)定了所有變量都存貯到主內存(如虛擬機物理內存中的一部分)中吉嚣。每一個線程都有一個自己的工作內存(如cpu中的高速緩存)梢薪。線程中的工作內存保存了該線程使用到的變量的主內存的副本拷貝。線程對變量的所有操作(讀取尝哆、賦值等)必須在該線程的工作內存中進行秉撇。不同線程之間無法直接訪問對方工作內存中變量。線程間變量的值傳遞均需要通過主內存來完成秋泄。
JMM定義了8種操作來控制變量讀寫琐馆。
lock(鎖定):作用于主內存,它把一個變量標記為一條線程獨占狀態(tài)恒序;
read(讀取):作用于主內存啡捶,它把變量值從主內存?zhèn)魉偷骄€程的工作內存中,以便隨后的load動作使用奸焙;
load(載入):作用于工作內存瞎暑,它把read操作的值放入工作內存中的變量副本中彤敛;
use(使用):作用于工作內存,它把工作內存中的值傳遞給執(zhí)行引擎了赌,每當虛擬機遇到一個需要使用這個變量的指令時候墨榄,將會執(zhí)行這個動作;
assign(賦值):作用于工作內存勿她,它把從執(zhí)行引擎獲取的值賦值給工作內存中的變量袄秩,每當虛擬機遇到一個給變量賦值的指令時候,執(zhí)行該操作逢并;
store(存儲):作用于工作內存之剧,它把工作內存中的一個變量傳送給主內存中,以備隨后的write操作使用砍聊;
write(寫入):作用于主內存背稼,它把store傳送值放到主內存中的變量中。
unlock(解鎖):作用于主內存玻蝌,它將一個處于鎖定狀態(tài)的變量釋放出來蟹肘,釋放后的變量才能夠被其他線程鎖定;
public class Jmm {
static volatile int shareVar = 0;
public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
while (shareVar == 0){
}
System.out.println("I known var updated");
}
}).start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
updateVarVal();
}
}).start();
}
public static void updateVarVal(){
shareVar = 1;
}
}
如上程序JMM控制的流程如下:
程序啟動后shareVar
將存入共享變量中俯树,同時兩個線程各自通過JMM操作將共享變量副本存入自己的工作內存帘腹,當值有更新的時候再刷入共享內存,并通知總線有值更新许饿,線程收到總線消息后把工作內存的值失效阳欲,然后從共享內存讀取最新的值。
線程生命周期
在Java程序中陋率,一個線程對象只能調用一次start()方法啟動新線程胸完,并在新線程中執(zhí)行run()方法。一旦run()方法執(zhí)行完畢翘贮,線程就結束了赊窥。因此,Java線程的狀態(tài)有以下幾種:
New 新創(chuàng)建的線程狸页,尚未執(zhí)行锨能;
Runnable 運行中的線程,正在執(zhí)行run()方法的Java代碼芍耘;
Blocked 運行中的線程址遇,因為某些操作被阻塞而掛起;
Waiting 運行中的線程斋竞,因為某些操作在等待中倔约;
Timed Waiting 運行中的線程,因為執(zhí)行sleep()方法正在計時等待坝初;
Terminated 線程已終止浸剩,因為run()方法執(zhí)行完畢钾军。
內存可見性
在Java中使用volatile
關鍵字來保證變量的一致性。
public class Volatile {
private static volatile int number = 0;
//每個線程會存一個變量副本
private static class ReaderThread extends Thread {
@Override
public void run() {
int local = number;
while (local < 5) {
if(local != number){
System.out.println("Got Change for MY_INT : " + number);
local = number;
}
}
}
}
private static class WriterThread extends Thread {
@Override
public void run() {
int local = number;
while (local < 5) {
System.out.println("Incrementing MY_INT to " + (local + 1));
number = ++local;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new ReaderThread().start();
new WriterThread().start();
System.out.println("end");
}
}
本案例中绢要,讀線程和寫線程共享變量number
吏恭,寫線程更新值,讀線程讀取值重罪。如果不用volatile
修飾共享變量樱哼,在寫線程更新了值后讀線程卻得不到最新的值,所以就有了可見性
問題剿配。因此搅幅,volatile
可以解決可見性和有序性的問題。
線程同步
Java通過synchronized
來實現(xiàn)線程的同步呼胚,也就是保證操作原子性茄唐。
public class SyncFunc {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new MyAddThread();
Thread t2 = new MyDecThread();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count is " + Counter.count);
Thread st1 = new SyncMyAddThread();
Thread st2 = new SyncMyDecThread();
st1.start();
st2.start();
st1.join();
st2.join();
System.out.println("count is " + SyncCounter.count);
}
static class Counter {
static int count = 0;
}
static class MyAddThread extends Thread{
public void run(){
for(int i = 0; i < 100000; i++){
Counter.count += 1;
}
}
}
static class MyDecThread extends Thread{
public void run(){
for(int i = 0; i < 100000; i++){
Counter.count -= 1;
}
}
}
static class SyncCounter {
final static Object lock = new Object();
static int count = 0;
}
static class SyncMyAddThread extends Thread{
public void run(){
for(int i = 0; i < 100000; i++){
//不同線程鎖同一個對象
synchronized (SyncCounter.lock){
SyncCounter.count += 1;
}
}
}
}
static class SyncMyDecThread extends Thread{
public void run(){
for(int i = 0; i < 100000; i++){
//不同線程鎖同一個對象
synchronized (SyncCounter.lock) {
SyncCounter.count -= 1;
}
}
}
}
}
本案例中有一個累加線程對對象變量count
進行累加,另外一個線程進行累減砸讳。一個場景沒加同步琢融,另外一個對對象加了同步界牡。因此得到的輸出是完全不一樣的簿寂,沒加同步的每次結果可能都不一樣,加了同步的結果都是0宿亡。需要注意的同步鎖住的對象級別的常遂,如果不同線程鎖的對象不是一個,會導致意外發(fā)生挽荠。
no synchronized: count is 10675
synchronized: count is 0
死鎖
死鎖是這樣一種情形:多個線程同時被阻塞克胳,它們中的一個或者全部都在等待某個資源被釋放。由于線程被無限期地阻塞圈匆,因此程序不可能正常終止漠另。
public class DeadLock {
public static void main(String[] args) {
Counter c1 = new Counter();
new Thread(() -> {
try {
c1.add(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
c1.dec(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
static class Lock{
static final Object lockA = new Object();
static final Object lockB = new Object();
}
static class Counter{
int valueA;
int valueB;
public void add(int m) throws InterruptedException {
System.out.println(new Date().toString() + " add 開始執(zhí)行");
synchronized (Lock.lockA){
this.valueA += m;
System.out.println(new Date().toString() + " add 鎖住 lockA");
Thread.sleep(3000); // 此處等待是給B能鎖住機
synchronized (Lock.lockB){
this.valueB += m;
System.out.println(new Date().toString() + " add 鎖住 lockB");
Thread.sleep(60 * 1000); // 為測試,占用了就不放
}
}
}
public void dec(int m) throws InterruptedException {
System.out.println(new Date().toString() + " dec 開始執(zhí)行");
synchronized (Lock.lockB){
this.valueA -= m;
System.out.println(new Date().toString() + " dec 鎖住 lockB");
Thread.sleep(3000); // 此處等待是給B能鎖住機
synchronized (Lock.lockA){
this.valueB -= m;
System.out.println(new Date().toString() + " dec 鎖住 lockA");
Thread.sleep(60 * 1000); // 為測試跃赚,占用了就不放
}
}
}
}
}
此案例中有兩個變量valueA
笆搓、valueB
和對象lockA
、lockB
纬傲。有兩個線程满败,一個線程先對對象lockA
加鎖,然后對變量valueA
累加操作和休眠一段時間(假設這過程比較耗時),再加鎖lockB
對象和操作valueB
;另外一個線程做相反操作叹括,先對對象localB
鎖和操作變量valueA
和休眠一段時間(假設這過程比較耗時)算墨,再加鎖lockA
和操作變量valueB
。這樣就會發(fā)現(xiàn)程序卡死了汁雷,分析java執(zhí)行日志信息可以發(fā)現(xiàn)發(fā)生死鎖的信息净嘀。通常發(fā)生死鎖需要三個條件:
競爭同一個資源
請求資源的方向相反
持續(xù)請求和保持
線程間通信
Java中通過notify
和wait
完成線程之間的通信报咳。
public class WaitAndNotify {
public static void main(String[] args) {
Task q = new Task();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
String s = null;
try {
s = q.get();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("get:" + s);
}).start();
}
new Thread(() -> {
for (int i = 0; i < 100; i++) {
String s = "t-" + i;
System.out.println("add task: " + s);
q.add(s);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}).start();
}
static class Task {
Queue<String> taskQueue = new LinkedList<>();
public synchronized void add(String s) {
taskQueue.add(s);
this.notifyAll();
}
public synchronized String get() throws InterruptedException {
while (taskQueue.isEmpty()) {
this.wait();
}
return taskQueue.remove();
}
}
}
此案例中有一個任務隊列taskQueue
,一個生產任務線程往隊列里添加任務,一個消費線程當隊列里有新任務時候取出面粮。所以當添加任務的時候調用notifyAll
通知所有監(jiān)聽的消費線程有新任務少孝,消費線程一直判斷隊列是否為空,如果為空就調用wait
來表示正在監(jiān)聽熬苍,不為空就取出任務稍走。
總結
不是多線程的程序,可以先寫代碼柴底,再重構優(yōu)化
多線程的程序婿脸,一定要先設計,再寫代碼
多線程編程的時候柄驻,要時刻想象同時很多個線程同時執(zhí)行時是否還能保證預期