1. 程式人生 > >Java併發程式設計與高併發解決方案解析

Java併發程式設計與高併發解決方案解析

本文轉載自:Java併發程式設計與高併發解決方案解析

現在在各大網際網路公司中,隨著日益增長的網際網路服務需求,高併發處理已經是一個非常常見的問題,在這篇文章裡面我們重點討論兩個方面的問題,一是併發程式設計,二是高併發解決方案。

文章中的程式碼實現詳見

專案 GitHub地址:https://github.com/YueMa233/concurrency.git

首先我們先來聊一聊併發的概念

併發:同時擁有兩個或多個執行緒,如果程式在單核處理器上執行,多個執行緒將交替地換入或者換出記憶體,這些執行緒是同時“存在”的,每個執行緒都處於執行過程中的某個狀態,如果執行在多核處理器上,此時,程式中的每個執行緒都將分配到一個處理器上,因此可以同時執行。

高併發:高併發(High Concurrency)是網際網路分散式系統架構設計中必須考慮的因素之一,它通常是指,通過保證系統能夠並行處理很多請求。

併發和高併發的側重點其實還是有一些細微的不同,在談到併發時候,我們側重於多個執行緒操作相同的資源,保證執行緒安全,合理利用資源。高併發是在系統執行的過程中短時間內遇到大量操作請求的情況,要求服務可以同時處理很多請求,提高程式效能。

知識技能

總體架構:SpringBoot、Maven、JDK8、MySQL

基礎元件:Mybatis、 Guava、Lombok、Redis、Kafka

高階元件:Joda-Time、Atomic包、J.U.C、AQS、ThreadLocal、RateLimiter、Hystrix、threadPool、shardbatis、elastic-job…

cpu多級快取

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

CPU快取有什麼意義

1)時間侷限性:如果某個資料被訪問,那麼在不久的將來它很可能被再次訪問;

2)空間侷限性:如果某個資料被訪問,那麼與它相鄰的資料很快也可能被訪問;

快取一致性協議(MESI)

用於保證多個CPU cache之間快取共享資料的一致
在這裡插入圖片描述亂序執行優化

處理器為提高運算速度而做出違背程式碼原有順序的優化
在這裡插入圖片描述


java記憶體模型(Java Memory Model)

Java虛擬機器規範中試圖定義一種Java記憶體模型來遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致性的記憶體訪問效果。

Java記憶體模型主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數的操作底層細節。此處的變數(Variables)與Java程式設計中的變數有所區別,它包括了例項欄位,靜態欄位,和構成陣列物件的元素,但不包括區域性變數與方法引數,因為後者是執行緒私有的,不會被共享,自然不會出現競爭問題。

![](https://img-blog.csdnimg.cn/20190104084920934.png?)
java記憶體模型 -同步八種操作
關主記憶體和工作記憶體之間的具體的互動協議,即一個變數如何從主記憶體拷貝到工作記憶體,如何從工作記憶體同步回主記憶體之類的實現細節,Java記憶體模型定義了以下8種操作來完成,虛擬機器實現時必須保證下面的每一種操作都是原子的、不可再分割的。

lock(鎖定):作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態。

unlock(解鎖):作用於主記憶體的變數,它把一個處於鎖定狀態之中的變數釋放出來,釋放出來後的變數可以被其他執行緒鎖定。

read(讀取):作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便後面load動作使用。

load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放到工作記憶體的變數副本中。

use(使用):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值得位元組碼指令時會執行這個操作。

assin(賦值):作用於工作記憶體的變數,它將一個執行位元組碼引擎接受到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。

store(儲存):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。

write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體得到的變數的值放入到主記憶體中去。

java記憶體模型 -同步規則

不允許read和load、store和write操作之一單獨出現。

不允許一個執行緒丟棄它的最近的assign操作,即變數變數在工作記憶體中改變了後必須把變化同步到主記憶體。

不允許一個執行緒無原因地把資料從執行緒的工作記憶體同步到主記憶體。

一個新的變數只能從主記憶體中“誕生”,不允許在工作記憶體中直接使用一個未被初始化的變數。

一個變數在同一時刻只允許對其進行lock操作,但lock操作可以被同一個執行緒執行多次,多次執行後只有執行相同次數的unlock操作變數才會被解鎖。

如果對一個變數執行lock操作,那將會清空工作記憶體中此變數的值,在執行引擎使用這個變數之前,需要重新執行load或assin初始化變數的值。

如果一個變數事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他執行緒鎖定住的變數。

對一個變數執行unlock之前,必須先將此變數同步回主記憶體中。

(詳情見JVM解析)…

併發的優勢與風險
在這裡插入圖片描述
併發程式設計

首先我們先建立一個SpringBoot的專案,在這個專案裡面我們進行一些併發操作。首先先構建一個專案,

專案構建的細節不在這裡討論直接給出專案框架以便於我們接下來的實戰。

專案啟動了
併發模擬-工具

Postman:Http請求模擬工具

Apache Bench(AB):Apache附帶的工具,測試網站效能

JMeter:Apache組織開發的壓力測試工具

併發模擬-程式碼:

CountDownLatch

CountDownLatch是一個同步工具類,用來協調多個執行緒之間的同步,或者說起到執行緒之間的通訊(而不是用作互斥的作用)。

CountDownLatch能夠使一個執行緒在等待另外一些執行緒完成各自工作之後,再繼續執行。使用一個計數器進行實現。計數器初始值為執行緒的數量。當每一個執行緒完成自己任務後,計數器的值就會減一。當計數器的值為0時,表示所有的執行緒都已經完成了任務,然後在CountDownLatch上等待的執行緒就可以恢復執行任務。

用法:

CountDownLatch典型用法1:某一執行緒在開始執行前等待n個執行緒執行完畢。將CountDownLatch的計數器初始化為nnew CountDownLatch(n),每當一個任務執行緒執行完畢,就將計數器減1countdownlatch.countDown(),當計數器的值變為0時,在CountDownLatch上await()的執行緒就會被喚醒。一個典型應用場景就是啟動一個服務時,主執行緒需要等待多個元件載入完畢,之後再繼續執行。

CountDownLatch典型用法2:實現多個執行緒開始執行任務的最大並行性。注意是並行性,不是併發,強調的是多個執行緒在某一時刻同時開始執行。類似於賽跑,將多個執行緒放到起點,等待發令槍響,然後同時開跑。做法是初始化一個共享的CountDownLatch(1),將其計數器初始化為1,多個執行緒在開始執行任務前首先coundownlatch.await(),當主執行緒呼叫countDown()時,計數器變為0,多個執行緒同時被喚醒。

Semaphore

Semaphore是計數訊號量。Semaphore管理一系列許可證。每個acquire方法阻塞,直到有一個許可證可以獲得然後拿走一個許可證;每個release方法增加一個許可證,這可能會釋放一個阻塞的acquire方法。然而,其實並沒有實際的許可證這個物件,Semaphore只是維持了一個可獲得許可證的數量。

Semaphore經常用於限制獲取某種資源的執行緒數量。

執行緒安全性

定義:當多個執行緒訪問某個類的時候,不管執行環境採用何種排程方式或者這些程序如何交替執行,並且在主調程式碼中不需要採用額外的同步或者是協同,這個類都能表現出正確的行為,那麼就稱這個類是執行緒安全的

原子性:提供了互斥訪問,同一時刻只能有一個執行緒來對它進行操作。

Atomic包

Atomic:CAS、Unsafe.compareAndSwapInt

AtmomicLong、LongAdder

AtomicReference、AtomicReferenceFieldUpdater

AtomicStampReference:CAS的ABA問題

Synchronized:依賴JVM

Lock:依賴特殊的CPU指令,程式碼實現,ReentrantLock

Synchronized

修飾程式碼塊:大括號括起來的程式碼,作用於呼叫的物件。

修飾方法:整個方法,作用於呼叫的物件。

修飾靜態方法:整個靜態方法,作用於所有物件。

修飾類:括號括起來的部分,作用於所有物件。

對比

synchronized:不可中斷鎖,適合競爭不激烈,可讀性好。

Lock:可中斷鎖,多樣化同步,競爭激烈時候能夠維持常態。

Atomic:競爭激烈時候可以維持常態,比Lock效能好;只能同步一個值。

可見性:可見性是指當一個執行緒修改了共享變數的值,其他執行緒可以立即得知這個修改。

導致共享變數線上程間不可見的原因

1)執行緒交叉執行。

2)重排序結合線程交叉執行。

3)共享變數更新後的值沒有在工作記憶體與主記憶體間及時更新

synchronized

1)執行緒解鎖前,必須把共享變數的最新值重新整理到主記憶體。

2)執行緒加鎖時,將清空工作記憶體中的值,從而使用共享變數時需要從主記憶體中重新讀取最新的值(加鎖解鎖是同一把鎖)。

volatile(通過加入記憶體屏障和禁止重排序優化實現)

1)對volatile變數寫操作時,會在寫操作後面加入一條store屏障指令,將本地記憶體中的共享變數重新整理到主記憶體。

不可保障在多執行緒計算中修飾值的安全性,不過它可以作為一個標識量。

volatile boolean inited = false;
 
//執行緒1
 
context = loadContext();
 
inited = true;
 
//執行緒2
 
while(!inited){
 
    sleep();
 
}
 
doSomethingWithConfig(context)

有序性:一個執行緒觀察其他執行緒中的指令執行順序,由於指令重排序的存在,該觀察結果一般都是雜亂無序。

java記憶體模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單執行緒程式的執行,卻會影響到多執行緒併發執行的正確性。

volatile、synchronized、Lock

先行發生原則

程式次序規律:一個執行緒內,按照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作。

鎖定規則:一個unlock操作先行發生於後面對同一個鎖的lock操作。

volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作。

傳遞規則:操作A先行發生於操作B,而操作B又先行發生於操作C、則可以得出A先行發生於C。

執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每一個動作。

執行緒中斷規則:對執行緒interrupt()方法呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生。

執行緒終止規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()返回值手段檢測執行緒已經終止執行。

物件終結規則:一個物件的初始化完成先行發生於它的finalize()方法的開始。

釋出物件:使一個物件能夠被當前範圍之外的程式碼所使用。

物件逸出:一種錯誤的釋出。當一個物件還沒有構造完成時,就使用它被其他執行緒所見。

安全釋出物件

在靜態初始化函式中初始化一個物件引用。

將物件的引用儲存到volatile型別域或者AtomicRefereence物件中。

將物件的引用儲存到某個正確構造物件的final型別域中。

將物件的引用儲存到一個由鎖保護的域中。

不可變物件

物件建立以後其狀態就不改變

物件所有域都是final型別

物件是正確建立的(在物件建立期間,this引用沒有逸出)

final關鍵字:類、方法、變數

修飾類:不能被繼承

修飾方法:1、鎖定方法不能被繼承類修改;2、效率

修飾變數:1、基本資料型別變數;2、引用型別(引用不可再引用,值還是可以變化)。

(其他不可變的兩種實現)

Collections.unmodifiableXXX:Collection、List、Set、Map…

Guava:ImmuableXXX:Collection、List、Set、Map…

執行緒封閉

Ad-hoc執行緒封閉:程式控制實現,最糟糕,忽略。

棧封閉:區域性變數,無併發問題。

ThreadLocal執行緒封閉:特別好的封閉方法。

執行緒不安全類與寫法

StringBuilder—StringBuffer

SimpleDataFormat—JodaTime

ArrayList、HashSet、HashMap等Collections

執行緒安全(同步容器)

ArrayList->Vector、Stack

HashMap->HashTable(key,value不能為null)

Collections.synchronizedXXX(List、Set、Map)

執行緒安全(併發容器 J.U.C)
在這裡插入圖片描述
ArrayList->CopyOnWriteArrayList(適合讀多寫少的場景、讀寫分離、讀不加鎖、寫加鎖)

HashSet、TreeSet->CopyOnWriteArraySet、ConcurrentSkipListSet

HashMap、TreeMap->ConcurrentHashMap、ConcurrentSkipListMap

AbstractQuenedSynchronizer-AQS(重中之重)在這裡插入圖片描述
1)使用Node實現FIFO佇列、可以用於構建鎖或者其他同步裝置的基礎框架。

2)利用了一個int型別表示狀態。

3)使用方法是繼承。

4)子類通過繼承實現它的方法管理其狀態{acquire和release}的方法操縱狀態。

5)可以同時實現排它鎖和共享鎖(獨佔、共享)

AQS實現的大致思路:

AQS內部維護了一個CLH佇列來管理鎖,執行緒會首先嚐試獲取鎖,如果失敗,會將當前執行緒以及等待狀態的資訊包裝成一個Node節點加入到同步佇列,接著不斷迴圈嘗試獲取鎖,它的條件是當前節點為head的直接後繼,如果失敗就會阻塞自己,直到自己被喚醒,當持有鎖的執行緒釋放鎖的時候會喚醒佇列中的後繼執行緒。

AQS同步元件

CountDownLatch(閉鎖,通過一計數來保證執行緒是否需要一直阻塞)

Semaphore(可以控制執行緒併發數目)

CyclicBarrier

ReentrantLock(重點)

Condition

FutureTask

CountDownLath(再次強調)在這裡插入圖片描述
CountDownLatch是一個同步工具類,用來協調多個執行緒之間的同步,或者說起到執行緒之間的通訊(而不是用作互斥的作用)。

CountDownLatch能夠使一個執行緒在等待另外一些執行緒完成各自工作之後,再繼續執行。使用一個計數器進行實現。計數器初始值為執行緒的數量。當每一個執行緒完成自己任務後,計數器的值就會減一。當計數器的值 為0時,表示所有的執行緒都已經完成了任務,然後在CountDownLatch上等待的執行緒就可以恢復執行任務。

用法:

CountDownLatch典型用法1:某一執行緒在開始執行前等待n個執行緒執行完畢。將CountDownLatch的計數器初始化為nnew CountDownLatch(n),每當一個任務執行緒執行完畢,就將計數器減1countdownlatch. countDown(),當計數器的值變為0時,在CountDownLatch上await()的執行緒就會被喚醒。一個典型應用場景就是啟動一個服務時,主執行緒需要等待多個元件載入完畢,之後再繼續執行。

CountDownLatch典型用法2:實現多個執行緒開始執行任務的最大並行性。注意是並行性,不是併發,強調的是多個執行緒在某一時刻同時開始執行。類似於賽跑,將多個執行緒放到起點,等待發令槍響,然後同時開 跑。做法是初始化一個共享的CountDownLatch(1),將其計數器初始化為1,多個執行緒在開始執行任務前首先coundownlatch.await(),當主執行緒呼叫countDown()時,計數器變為0,多個執行緒同時被喚醒。

Semaphore(再次強調)在這裡插入圖片描述
Semaphore是計數訊號量。Semaphore管理一系列許可證。每個acquire方法阻塞,直到有一個許可證可以獲得然後拿走一個許可證;每個release方法增加一個許可證,這可能會釋放一個阻塞的acquire方法。然而,其實並沒有實際的許可證這個物件,Semaphore只是維持了一個可獲得許可證的數量。

Semaphore經常用於限制獲取某種資源的執行緒數量。

CyclicBarrier在這裡插入圖片描述
類似於CountDownLatch,不過可以迴圈。

CountDownLatch描述的是一個或n個執行緒需要等待其他執行緒完成某個操作之後才能往下執行;CyclicBarrier描述的是多個執行緒之間等待直到所有執行緒都滿足條件後才可執行。

ReentrantLock與鎖

ReentrankLock(可重入鎖)和synchronized的區別

可重入性

鎖的實現

效能區別

功能區別

ReentrantLock獨有的功能

可以指定公平鎖或是非公平鎖。

提供了一個Condition類,可以分組喚醒需要喚醒的執行緒。

提供能夠中斷等待鎖的執行緒的機制,Lock.lockInterruptibly()。

public ReentrantLock(boolean fair) {
 
sync = fair  new FairSync() : new NonfairSync();
 
}

Condition

實現一個簡單的有界佇列,佇列為空時,佇列的刪除操作將會阻塞直到佇列中有新的元素,佇列已滿時,佇列的插入操作將會阻塞直到隊列出現空位。

FutureTask
在這裡插入圖片描述
詳情可以參考一位老哥的部落格:

https://www.cnblogs.com/dolphin0520/p/3949310.html

Fork/Join框架(工作竊取演算法)
在這裡插入圖片描述詳情可以參考另一位老哥的部落格:

https://blog.csdn.net/qq_39266910/article/details/79928177

BlockingQueue(阻塞佇列)在這裡插入圖片描述
實現類

ArrayBlockingQueue(有界的阻塞佇列,資料結構陣列)

DelayQueue

LinkedBlockingQueue(資料結構連結串列)

PriorityBlockingQueue(無界)

SynchronousQueue(同步只能放一個元素)

執行緒池

new Thread 弊端

每次new Thread新建物件,效能差。

執行緒缺乏統一管理,可能無限制的新建執行緒,相互競爭,有可能佔用過多系統資源導致宕機或OOM。

缺少更多功能,如更多執行定期執行,執行緒中斷。

執行緒池的好處

重用存在的執行緒,減少物件的建立,消亡的開銷,效能佳。

可有效控制最大併發執行緒數,提高系統資源利用率,同時可以避免過多資源競爭,避免阻塞。

提供定時執行,定期執行,單執行緒,併發數控制等功能。

ThreadPoolExecutor

corePoolSize:核心執行緒數

maximumPoolSize:執行緒最大執行緒數

workQueue:阻塞佇列,儲存等待執行的任務,很重要,會對執行緒池執行過程產生重大影響

(任務提交時可以分三種情況:1、當任務提交時執行緒數小於corePoolSize不管有沒有空閒執行緒直接新建執行緒;2、如果執行緒數量大於等於corePoolSize且小於maximumPoolSize則只有當workQueue滿的時候新建執行緒去處理任務;3、若corePoolSize和maximumPoolSize設定大小一樣時,說明執行緒池大小固定,有新任務時看workQueue中任務是否有空餘,若有:放任務進去,若沒有且執行緒數大於maximumPoolSize則通過一個叫做“拒絕策略的引數”指定策略去處理任務。)

keepAliveTime:執行緒沒有任務執行時最多保持多久時間終止

(當執行緒池中的數量大於corePoolSize時,如果沒有新的任務提交,核心執行緒不會立即銷燬,會等待一段時間。)

util:keepAliveTime的時間單位

threadFactory:執行緒工廠,用來建立執行緒

rejectHandler:當拒絕任務時的策略

(直接丟擲異常、用呼叫者所在的執行緒執行任務、丟棄佇列最靠前的任務並執行當前任務、直接丟棄這個任務)

看完了這幾個引數之後我們再來看看下面的幾個方法:

execute():提交任務,交給執行緒池執行

submit():提交任務,能夠返回結果execute+Future

shutdown():關閉執行緒池,等待任務都執行完

shutdownNow:關閉執行緒池,不等待任務執行完

getTaskCount():執行緒池已執行和未執行的任務總數

getCompletedTaskCount():已完成的任務數量

getPoolSize():執行緒池當前執行緒數量

getActiveCount():當前執行緒池中正在執行任務的執行緒總數在這裡插入圖片描述Executor框架介面

Executors.newCachedThreadPool(可快取執行緒池)

Executors.newFixedThreadPool(定長執行緒池)

Executors.newScheduledThreadPool(定長執行緒池,支援定時週期執行)

Executors.newSingleThreadExcutor(單執行緒執行緒池)

執行緒池-合理配置

CPU密集型任務,就需要儘量壓榨CPU,參考值可以設為NCPU+1

IO密集型任務,參考值可以設為2*NCPU

死鎖

兩個或兩個以上程序在執行任務的時候由於爭奪資源等待資源而發生互相等待的狀態。

四個必要條件

互斥條件(程序對鎖分配的資源排他性的使用,即在一段時間內某資源只由一個程序佔用,如果還有其他程序想要獲得資源只能等待)

請求和保持條件(程序已經保持了至少一個資源但還申請了新的資源請求,而該資源已被其他程序佔有,此時請求程序阻塞,但又對自己保持資源保持不放)

不剝奪條件(程序佔用的資源在未使用完之前不可被剝奪,自己使用完之後釋放)

環路等待條件(發生死鎖時一定有程序存在資源環形的鏈)

死鎖實現程式碼

package com.ma.concurrency.example.deadLock;
 
 
import lombok.extern.slf4j.Slf4j;
 
 
/**
 
* 一個簡單的死鎖類
 
* 當DeadLock類的物件flag==1(td1),先鎖定o1,睡眠500毫秒
 
* 而當td1在睡眠的時候另一個flag==0的物件(td2)執行緒啟動,先鎖定o2,睡眠500毫秒
 
* td1睡眠結束後需要鎖定o2才能繼續執行,而此時o2已被td2鎖定;
 
* td2睡眠結束後需要鎖定o1才能繼續執行,而此時o1已被td1鎖定;
 
* td1、td2相互等待,都需要對方鎖定的資源才能繼續執行,從而發生死鎖
 
*/
 
@Slf4j
 
public class DeadLock implements Runnable{
 
public int flag = 1;//靜態物件是類的所有物件共享的
 
private static Object o1 = new Object(),o2 = new Object();
 
 
@Override
 
public void run() {
 
log.info("flag:{}",flag);
 
if(flag == 1){
 
synchronized (o1){
 
try {
 
Thread.sleep(5000);
 
} catch (InterruptedException e) {
 
e.printStackTrace();
 
}
 
synchronized (o2){
 
log.info("1");
 
}
 
}
 
}
 
if(flag == 0) {
 
synchronized (o2) {
 
try {
 
Thread.sleep(5000);
 
} catch (InterruptedException e) {
 
e.printStackTrace();
 
}
 
synchronized (o1) {
 
log.info("0");
 
}
 
}
 
}
 
}
 
 
public static void main(String[] args) {
 
DeadLock td1 = new DeadLock();
 
DeadLock td2 = new DeadLock();
 
td1.flag = 1;
 
td2.flag = 0;
 
//td1,td2都處於可執行狀態,但JVM執行緒排程先執行哪個執行緒是不確定的。
 
//td2的run()可能在td1的run()之前執行
 
new Thread(td1).start();
 
new Thread(td2).start();
 
}
}

如何避免死鎖

1)執行緒按順序加鎖時候注意順序

2)加鎖實現

3)死鎖檢測

以上併發程式設計的部分我們已經學到了不少的知識,接下來我們聊聊多執行緒併發在實踐中採用哪些方式較好

1)使用本地變數

2)使用不可變類

3)最小化鎖的作用域範圍:S=1/(1-a+a/n) a:並行處理所佔的比例;n:並行節點的個數;S:加速比

4)使用執行緒池的Executor,而不是直接new Thread操作

5)寧可使用同步也不要使用執行緒的wait和notify

6)使用BlockingQueue實現生產-消費模式

7)使用同步集合而不是加了鎖的同步集合

8)使用Semaphore建立有界的訪問

9)寧可使用同步程式碼塊也不使用同步方法

10)避免使用靜態變數

Spring與執行緒安全

Spring bean:single、prototype

無狀態物件

HashMap和ConcurrentHashMap

HashMap

存值操作

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 
boolean evict) {
 
Node<k,v>[] tab; Node<k,v> p; int n, i;
 
if ((tab = table) == null || (n = tab.length) == 0)
 
n = (tab = resize()).length;
 
if ((p = tab[i = (n - 1) & hash]) == null)
 
tab[i] = newNode(hash, key, value, null);
 
else {
 
Node<k,v> e; K k;
 
if (p.hash == hash &&
 
((k = p.key) == key || (key != null && key.equals(k))))
 
e = p;
 
else if (p instanceof TreeNode)
 
e = ((TreeNode<k,v>)p).putTreeVal(this, tab, hash, key, value);
 
else {
 
for (int binCount = 0; ; ++binCount) {
 
if ((e = p.next) == null) {
 
p.next = newNode(hash, key, value, null);
 
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
 
treeifyBin(tab, hash);
 
break;
 
}
 
if (e.hash == hash &&
 
((k = e.key) == key || (key != null && key.equals(k))))
 
break;
 
p = e;
 
}
 
}
 
if (e != null) { // existing mapping for key
 
V oldValue = e.value;
 
if (!onlyIfAbsent || oldValue == null)
 
e.value = value;
 
afterNodeAccess(e);
 
return oldValue;
 
}
 
}
 
++modCount;
 
if (++size > threshold)
 
resize();
 
afterNodeInsertion(evict);
 
return null;
 
}</k,v></k,v></k,v></k,v>

單執行緒下HashMap的擴容是安全的在這裡插入圖片描述多執行緒下的HashMap擴容可能會發生迴圈問題
1,在這裡插入圖片描述
2,在這裡插入圖片描述
3,在這裡插入圖片描述
4,在這裡插入圖片描述ConcurrentHashMap

java7的Segment()技術在這裡插入圖片描述
在Java8中為了進一步提高併發度當陣列超過8(預設)會轉化為紅黑樹定址時間複雜度從O(N)變化為O(logN)
在這裡插入圖片描述
回顧我們在上面學習的一些東西:在這裡插入圖片描述
接下來我們進入第二階段:高併發的解決方案,我們主要學習高併發處理思路與手段

擴容

垂直擴容(縱向擴充套件):提高系統部件能力

水平擴容(橫向擴容):增加更多系統成員來實現(叢集)

擴容-資料庫

讀操作擴充套件:memcache、redis、CDN等快取

寫操作擴充套件:Cassandra、Hbase等

快取特徵

命中率:命中數/(命中數+沒有命中數)

最大元素(空間)

清空策略:FIFO(先進先出)、LFU(最少使用)、LRU(最近最少使用)、過期時間、隨機等

快取命中率的影響因素

業務場景和業務需求

快取的設計(粒度和策略)

快取容量和基礎設施

快取分類和應用場景

本地快取:程式設計實現(成員變數、區域性變數、靜態變數)Guava Cache

分散式快取:Memcache、Redis

Guava Cache

快取是日常開發中經常應用到的一種技術手段,合理的利用快取可以極大的改善應用程式的效能。

Guava官方對Cache的描述連線

快取在各種各樣的用例中非常有用。例如,當計算或檢索值很昂貴時,您應該考慮使用快取,並且不止一次需要它在某個輸入上的值。

快取ConcurrentMap要小,但不完全相同。最根本的區別在於一個ConcurrentMap堅持所有新增到它直到他們明確地刪除元素。

另一方面,快取一般配置為自動退出的條目,以限制其記憶體佔用。在某些情況下,一個LoadingCache可以即使不驅逐的條目是有用的,因為它的自動快取載入。

一位老哥的部落格:

https://blog.csdn.net/u012881904/article/details/79263787

Memcache

memcache是一套分散式的快取記憶體系統,由LiveJournal的Brad Fitzpatrick開發,但目前被許多網站使用以提升網站的訪問速度,尤其對於一些大型的、需要頻繁訪問資料庫的網站訪問速度提升效果十分顯著 [1] 。這是一套開放原始碼軟體,以BSD license授權釋出。

MemCache的工作流程如下:先檢查客戶端的請求資料是否在memcached中,如有,直接把請求資料返回,不再對資料庫進行任何操作;如果請求的資料不在memcached中,就去查資料庫,把從資料庫中獲取的資料返回給客戶端,同時把資料快取一份到memcached中(memcached客戶端不負責,需要程式明確實現);每次更新資料庫的同時更新memcached中的資料,保證一致性;當分配給memcached記憶體空間用完之後,會使用LRU(Least Recently Used,最近最少使用)策略加上到期失效策略,失效資料首先被替換,然後再替換掉最近未使用的資料。 [2]

Memcache是一個高效能的分散式的記憶體物件快取系統,通過在記憶體裡維護一個統一的巨大的hash表,它能夠用來儲存各種格式的資料,包括影象、視訊、檔案以及資料庫檢索的結果等。簡單的說就是將資料呼叫到記憶體中,然後從記憶體中讀取,從而大大提高讀取速度。

Memcache是danga的一個專案,最早是LiveJournal 服務的,最初為了加速 LiveJournal 訪問速度而開發的,後來被很多大型的網站採用。

Memcached是以守護程式(監聽)方式運行於一個或多個伺服器中,隨時會接收客戶端的連線和操作。

一位老哥的部落格:

https://www.2cto.com/database/201703/611509.html

Redis

redis是一個key-value儲存系統。和Memcached類似,它支援儲存的value型別相對更多,包括string(字串)、list(連結串列)、set(集合)、zset(sorted set --有序集合)和hash(雜湊型別)。這些資料型別都支援push/pop、add/remove及取交集並集和差集及更豐富的操作,而且這些操作都是原子性的。在此基礎上,redis支援各種不同方式的排序。與memcached一樣,為了保證效率,資料都是快取在記憶體中。區別的是redis會週期性的把更新的資料寫入磁碟或者把修改操作寫入追加的記錄檔案,並且在此基礎上實現了master-slave(主從)同步。

Redis 是一個高效能的key-value資料庫。 redis的出現,很大程度補償了memcached這類key/value儲存的不足,在部 分場合可以對關係資料庫起到很好的補充作用。它提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客戶端,使用很方便。 [1]

Redis支援主從同步。資料可以從主伺服器向任意數量的從伺服器上同步,從伺服器可以是關聯其他從伺服器的主伺服器。這使得Redis可執行單層樹複製。存檔可以有意無意的對資料進行寫操作。由於完全實現了釋出/訂閱機制,使得從資料庫在任何地方同步樹時,可訂閱一個頻道並接收主伺服器完整的訊息釋出記錄。同步對讀取操作的可擴充套件性和資料冗餘很有幫助。

redis的官網地址,非常好記,是redis.io。(特意查了一下,域名字尾io屬於國家域名,是british Indian Ocean territory,即英屬印度洋領地)

目前,Vmware在資助著redis專案的開發和維護。

一位老哥的部落格:

https://blog.csdn.net/liqingtx/article/details/60330555

Redis和Memcache區別

1.Redis是什麼

這個問題的結果影響了我們怎麼用Redis。如果你認為Redis是一個key value store, 那可能會用它來代替MySQL;如果認為它是一個可以持久化的cache, 可能只是它儲存一些頻繁訪問的臨時資料。Redis是REmote DIctionary Server的縮寫,在Redis在官方網站的的副標題是A persistent key-valuedatabase with built-in net interface written in ANSI-C for Posix systems,這個定義偏向key value store。還有一些看法則認為Redis是一個memory database,因為它的高效能都是基於記憶體操作的基礎。另外一些人則認為Redis是一個data structure server,因為Redis支援複雜的資料特性,比如List, Set等。對Redis的作用的不同解讀決定了你對Redis的使用方式。

網際網路資料目前基本使用兩種方式來儲存,關係資料庫或者key value。但是這些網際網路業務本身並不屬於這兩種資料型別,比如使用者在社會化平臺中的關係,它是一個list,如果要用關係資料庫儲存就需要轉換成一種多行記錄的形式,這種形式存在很多冗餘資料,每一行需要儲存一些重複資訊。如果用key value儲存則修改和刪除比較麻煩,需要將全部資料讀出再寫入。Redis在記憶體中設計了各種資料型別,讓業務能夠高速原子的訪問這些資料結構,並且不需要關心持久儲存的問題,從架構上解決了前面兩種儲存需要走一些彎路的問題。

  1. Redis不可能比Memcache快

很多開發者都認為Redis不可能比Memcached快,Memcached完全基於記憶體,而Redis具有持久化儲存特性,即使是非同步的,Redis也不可能比Memcached快。但是測試結果基本是Redis佔絕對優勢。一直在思考這個原因,目前想到的原因有這幾方面。

Libevent。和Memcached不同,Redis並沒有選擇libevent。Libevent為了迎合通用性造成程式碼龐大(目前Redis程式碼還不到libevent的1/3)及犧牲了在特定平臺的不少效能。Redis用libevent中兩個檔案修改實現了自己的epoll event loop(4)。業界不少開發者也建議Redis使用另外一個libevent高效能替代libev,但是作者還是堅持Redis應該小巧並去依賴的思路。一個印象深刻的細節是編譯Redis之前並不需要執行./configure。

CAS問題。CAS是Memcached中比較方便的一種防止競爭修改資源的方法。CAS實現需要為每個cache key設定一個隱藏的cas token,cas相當value版本號,每次set會token需要遞增,因此帶來CPU和記憶體的雙重開銷,雖然這些開銷很小,但是到單機10G+ cache以及QPS上萬之後這些開銷就會給雙方相對帶來一些細微效能差別(5)。

  1. 單臺Redis的存放資料必須比實體記憶體小

Redis的資料全部放在記憶體帶來了高速的效能,但是也帶來一些不合理之處。比如一箇中型網站有100萬註冊使用者,如果這些資料要用Redis來儲存,記憶體的容量必須能夠容納這100萬用戶。但是業務實際情況是100萬用戶只有5萬活躍使用者,1周來訪問過1次的也只有15萬用戶,因此全部100萬用戶的資料都放在記憶體有不合理之處,RAM需要為冷資料買單。

這跟作業系統非常相似,作業系統所有應用訪問的資料都在記憶體,但是如果實體記憶體容納不下新的資料,作業系統會智慧將部分長期沒有訪問的資料交換到磁碟,為新的應用留出空間。現代作業系統給應用提供的並不是實體記憶體,而是虛擬記憶體(Virtual Memory)的概念。

基於相同的考慮,Redis 2.0也增加了VM特性。讓Redis資料容量突破了實體記憶體的限制。並實現了資料冷熱分離。

  1. Redis的VM實現是重複造輪子

Redis的VM依照之前的epoll實現思路依舊是自己實現。但是在前面作業系統的介紹提到OS也可以自動幫程式實現冷熱資料分離,Redis只需要OS申請一塊大記憶體,OS會自動將熱資料放入實體記憶體,冷資料交換到硬碟,另外一個知名的“理解了現代作業系統(3)”的Varnish就是這樣實現,也取得了非常成功的效果。

作者antirez在解釋為什麼要自己實現VM中提到幾個原因(6)。主要OS的VM換入換出是基於Page概念,比如OS VM1個Page是4K, 4K中只要還有一個元素即使只有1個位元組被訪問,這個頁也不會被SWAP, 換入也同樣道理,讀到一個位元組可能會換入4K無用的記憶體。而Redis自己實現則可以達到控制換入的粒度。另外訪問作業系統SWAP記憶體區域時block程序,也是導致Redis要自己實現VM原因之一。

  1. 用get/set方式使用Redis

作為一個key value存在,很多開發者自然的使用set/get方式來使用Redis,實際上這並不是最優化的使用方法。尤其在未啟用VM情況下,Redis全部資料需要放入記憶體,節約記憶體尤其重要。

假如一個key-value單元需要最小佔用512位元組,即使只存一個位元組也佔了512位元組。這時候就有一個設計模式,可以把key複用,幾個key-value放入一個key中,value再作為一個set存入,這樣同樣512位元組就會存放10-100倍的容量。

這就是為了節約記憶體,建議使用hashset而不是set/get的方式來使用Redis,詳細方法見參考文獻(7)。

  1. 使用aof代替snapshot

Redis有兩種儲存方式,預設是snapshot方式,實現方法是定時將記憶體的快照(snapshot)持久化到硬碟,這種方法缺點是持久化之後如果出現crash則會丟失一段資料。因此在完美主義者的推動下作者增加了aof方式。aof即append only mode,在寫入記憶體資料的同時將操作命令儲存到日誌檔案,在一個併發更改上萬的系統中,命令日誌是一個非常龐大的資料,管理維護成本非常高,恢復重建時間會非常長,這樣導致失去aof高可用性本意。另外更重要的是Redis是一個記憶體資料結構模型,所有的優勢都是建立在對記憶體複雜資料結構高效的原子操作上,這樣就看出aof是一個非常不協調的部分。

其實aof目的主要是資料可靠性及高可用性,在Redis中有另外一種方法來達到目的:Replication。由於Redis的高效能,複製基本沒有延遲。這樣達到了防止單點故障及實現了高可用。

高併發場景下快取常見問題

快取一致性在這裡插入圖片描述
快取併發問題
在這裡插入圖   
 
 </div> 
 <div class=

相關推薦

長文慎入-探索Java併發程式設計併發解決方案[轉]

轉自:https://yq.aliyun.com/articles/636038 所有示例程式碼,請見/下載於https://github.com/Wasabi1234/concurrency   高併發處理的思路及手段    

併發程式設計併發解決方案學習(Java 記憶體模型)

JMM(Java Memory Model)    JMM是一種規範,規範了Java虛擬機器與計算機記憶體是如何協同工作的,規定了一個執行緒如何和何時可以看到其他執行緒修改過的共享變數的值,以及在必須的時候如果同步的訪問共享變數。棧    棧的優勢:存取速度比堆要快,僅次於計

Java併發程式設計併發解決方案解析

本文轉載自:Java併發程式設計與高併發解決方案解析 現在在各大網際網路公司中,隨著日益增長的網際網路服務需求,高併發處理已經是一個非常常見的問題,在這篇文章裡面我們重點討論兩個方面的問題,一是併發程式設計,二是高併發解決方案。 文章中的程式碼實現詳見 專案 Git

併發程式設計併發解決方案學習(併發程式設計初體驗)

以下都是發生執行緒安全的案例: 模擬5000個請求,併發數200 package vip.fkandy.chapter02; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDown

併發程式設計併發解決方案學習(同步容器)

ArrayList->Vector,Stack HashMap->HashTable(key,value不能為null) Collections.synchronizedXXX(List、Set、Map) [java]  view pl

2019最新Java併發程式設計併發解決教程

併發: 同時擁有兩個或者多個執行緒,如果程式在單核處理器上執行多個執行緒將交替地換入或者換出記憶體,這些執行緒是 同時“存在”的,每個執行緒都處於執行過程中的某個狀態,如果執行在多核處理器上,此時,程式中的每個執行緒都 將分配到一個處理器核上,因此可以同時執行。高併發( Hi

併發程式設計併發解決方案學習(常見類執行緒安全性研究)

StringBuilder 非執行緒安全import com.mmall.concurrency.annoations.NotThreadSafe; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.

併發程式設計併發解決方案學習(併發併發基本概念)

一、概念    併發:同時擁有兩個或兩個以上執行緒,如果程式在單核處理器上執行,多個執行緒將替換地換入或者換出記憶體,這些執行緒是同時"存在"的,每個執行緒都處於執行過程中的某個狀態,如果執行在多核處理

Java併發程式設計併發學習總結(一)-大綱

系列 開篇語 想寫這樣一個東西很久了,在慕課網上學完某老師的課程(避免打廣告的嫌疑就不貼出來了,感興趣的同學可以去慕課網上去搜來看看,是個付費課程)之後就覺得應該有這樣的一個學習總結的東西來,後來因為懶又有其他事情耽誤了,然後又上了新專案(正好拿來練手了,當然

Java併發程式設計技術內幕:ConcurrentHashMap原始碼解析

       摘要:本文主要講了Java中ConcurrentHashMap 的原始碼       ConcurrentHashMap 是java併發包中非常有用的一個類,在高併發場景用得非常多,它是執行緒安全的。要注意到雖然HashTable雖然也是執行緒安全的,但是它的效

併發程式設計併發場景:秒殺(無鎖、排他鎖、樂觀鎖、redis快取的逐步演變)

環境: jdk1.8;spring boot2.0.2;Maven3.3 摘要說明: 在實際開發過程中往往會出現許多高併發場場景,秒殺,強紅包,搶優惠卷等; 其中: 秒殺場景的特點就是單位時間湧入使用者量極大,商品數少,且要保證不可超量銷售; 秒殺產品的本質就是減

【2019春招準備:23. 併發程式設計併發總結】

【內容】 A.併發 執行緒安全 執行緒封閉 執行緒排程 同步容器 併發容器 AQS J.U.C

Java併發程式設計入門併發面試

第6章 J.U.C之AQS講解 AQS是J.U.C的重要元件,也是面試的重要考點。這一章裡將重點講解AQS模型設計及相關同步元件的原理和使用,都非常實用,具體包括:CountDownLatch、Semaphore、CyclicBarrier、ReentrantLock與鎖、Condition等。這些元

JAVA中高訪問量併發的問題的一部分解決方案

個人見解,希望大家多多提出意見 1.儘量使用快取技術來做。使用者快取、頁面快取等一切快取,使用特定的機制進行重新整理。利用消耗記憶體空間來換取使用者的效率。同時減少資料庫的訪問次數。 2.把資料庫的查詢語句進行優化,一般複雜的SQL語句就不要使用ORM框架自帶的做法

併發大資料解決方案概述

概述 隨著業務的不斷豐富,高併發和海量資料的處理日益成為影響系統性能的重要問題。下面將提供一些針對併發問題和海量資料處理的解決方案。 海量資料的解決方案: 快取 頁面靜態化 資料庫優化 分離活躍資料 批量讀取和延遲修改 讀寫分離 分散式資料

java併發(四)併發程式設計執行緒安全

    程式碼有多個執行緒同時執行,而這些執行緒可能會同時運行同一段程式碼,如果每次執行的結果和單執行緒執行的

總結-Java多執行緒併發簡記

1、什麼是多執行緒? 一個程序可以開啟多個執行緒,每個執行緒可以併發/並行執行不同任務。 2、Java多執行緒實現方式    2.1、繼承Thread類    2.2、實現Runnable介面方式實現多執行緒    2.3、使

Web大規模併發請求和搶購的解決方案

電商的秒殺和搶購,對我們來說,都不是一個陌生的東西。然而,從技術的角度來說,這對於Web系統是一個巨大的考驗。當一個Web系統,在一秒鐘內收到數以萬計甚至更多請求時,系統的優化和穩定至關重要。這次我們會關注秒殺和搶購的技術實現和優化,同時,從技術層面揭開,為什麼我們總是不容易搶到火車票的原因

深入理解併發下分散式事務的解決方案

1、什麼是分散式事務 分散式事務就是指事務的參與者、支援事務的伺服器、資源伺服器以及事務管理器分別位於不同的分散式系統的不同節點之上。以上是百度百科的解釋,簡單的說,就是一次大的操作由不同的小操作組成,這些小的操作分佈在不同的伺服器上,且屬於不同的應用,分散式事務需要保證這

併發的詳解及解決方案

一、什麼是高併發 高併發(High Concurrency)是網際網路分散式系統架構設計中必須考慮的因素之一,它通常是指,通過設計保證系統能夠同時並行處理很多請求。   高併發相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Quer