什麼是高併發,怎麼解決高併發

陌小伊發表於2020-10-30

當提到高併發的時候,很多人就有疑問,到底什麼是高併發程式設計?

以登入功能為例。當登入的時候,是使用者拿使用者名稱,密碼到資料庫裡訪問是否存在,存在則跳轉到登入頁面。然後修改訪問次數為+1.否則跳轉到失敗頁面,訪問次數不加1.

當一個使用者進行訪問的時候,是不存在併發性問題的。因為使用者查詢庫表,修改訪問次數,不會受到別人的影響。

但是當兩個使用者訪問的時候,在查詢庫表的時候,假定兩個使用者是順序的。第一個登陸進來,完成了修改登陸次數的操作之後,第二個使用者才登陸進來。那麼這個也是不存在併發性問題的。

那什麼時候出現的併發性問題呢?當有很多個使用者同時登陸,假定恰好一個使用者剛查詢到自己的賬號和密碼,然後把登陸次數從庫表讀到了記憶體中,還沒來得及該表,就是說還沒來得及修改登陸次數,這個時候又來了一個使用者查詢自己的賬號和密碼,雖然後面的賬號來的晚,但由於存在表中的順序靠前,查詢的塊,接下來去表裡取登陸次數早於第一個使用者提交結果到表(實際上和第一個使用者獲取到的登陸次數是一樣的,因為第一個使用者還沒來得及修改表裡的這條記錄),然後再到庫表中修改自己的登陸次數。這個時候,當第一個使用者再修改登陸次數的時候,由於是基於自己讀取到的登陸次數進行加1的操作,就會丟失掉第二個使用者的登陸次數。實際上兩個使用者雖然都登陸了,但實際上只記錄了一個使用者的登陸次數(丟失修改)。如果同時登陸的使用者數非常多(例如一毫秒1個(一次資料庫操作需要幾個毫秒)),就會出現很多這類問題。

上面是併發造成的問題之一,是資料安全性問題。究其原因是什麼呢?是因為登陸次數是很多個使用者共享的,而且是共享修改和讀取的。為什麼同樣高併發的使用者登陸,對於登陸的賬號不會出現安全性問題呢,因為不涉及修改操作,也不涉及共享資料(查詢的不是同一條記錄,且結果不相互影響)。因此可以這麼下結論:只有出現共享資料的問題才會有併發的資料安全性問題。

當然,併發的資料安全性問題不僅僅侷限於上述的場景,還會包含,諸如髒讀等問題。

那麼我把登陸次數的訪問和修改加鎖,是否可以完全解決併發性的資料安全性問題呢?通過一個鎖來控制對登陸次數的修改,一次只能一個使用者修改,直到修改完登陸次數,釋放鎖,才允許下個使用者訪問。

這完。但又有一個新的全可以解決一部分安全性問題問題,鎖也會成為多個執行緒的共享資料,既然鎖是共享資料,也不可避免的出現了併發修改的問題,那再對鎖加鎖的話,顯然就進入極限但不收斂的狀態了。不是100%可靠。

很多人不理解,鎖為什麼也會導致併發不安全?鎖讀的時候會進行判斷的啊,為何還不行?這裡就有一個很隱晦的問題了:指令優化。

正常情況下,我們程式碼順序執行,先判斷鎖的狀態,是否鎖住了。然後如果沒有鎖住,則進入,否則持續間隔時間訪問鎖的狀態,直到鎖被其他執行緒釋放,然後才進入。按理說不存在像從資料庫表裡讀記錄到記憶體,會出現時間差的問題,導致順序出現差異,為何還會有併發性問題呢?

這裡得講一個原理:jvm指令優化。

我們知道,.java字尾的原始碼要被java虛擬機器執行,需要進行編譯成 .class 字尾的檔案。那這個class檔案要被虛擬機器執行,實際上裡面的程式碼指令,不再是我們寫的那些 public,static main String int等關鍵字了。會被轉換成 另外的一個指令集(如load,read,reload等)這個指令集的轉換,是編譯器進行的。我們直觀的理解,編譯器會按照順序編譯,即編譯器會根據某個順序將我們的一行java程式碼轉換成class字尾的檔案。可實際上並非如此。實際上編譯器有編譯語法,也有優化語法。會根據具體場景做一些執行順序上的優化。這些順序上的優化,可能是將兩次相鄰的讀一個資料的操作合併為一個語句進行執行(單執行緒情況下,編譯器判斷為指令重複,將兩條對某個資料讀操作相鄰(可能中間含有其他資料的其他操作,但也認為是相鄰)進行優化成了一條讀指令)。但在併發情況下,這種合併會出現諸如上面兩次讀取,中間另外一個執行緒修改資料而導致結果不一樣的情況。是不能進行簡化成一次讀取的。所以就出現了,優化後的語句,執行在併發情況下,是和順序執行的順序不一致的結果。所以我們說,是由於jvm指令編譯的優化,導致了鎖併發的失效。

如果你讀過java併發程式設計的藝術一書,可能知道這個時候應該用volatile關鍵字修飾鎖,不允許jvm優化。這總可以解決併發問題了吧。

那是否就能萬無一失了呢?答案告訴你,還是否定的。這是為什麼呢?

再講一個原理:彙編指令優化

我們知道jvm是作業系統層面的執行指令集。實際上,我們執行指令最終是硬體的電氣特性。這個電器特性在執行的過程中,只認一個東西,就是01.指令。那麼我們jvm層面的class檔案對應的指令集(諸如load,read等)是如何被執行的呢?答案是,先有編譯器,轉換成彙編指令,再由編譯器轉換成二進位制指令。

剛才我們說了,volatile可以禁止掉jmv編譯時進行優化,那彙編的過程中,實際上也是有類似的相同的問題的,也是會進行相似的優化指令執行的順序的。當合並兩次讀操作的時候,同樣在另外一個執行緒在兩次讀操作之間進行了寫入(這裡的是class指令的read,load),會導致兩次結果不一致的情況。這時,雖然加了volatile關鍵字。或者使用的是原子變數(也是禁止編譯時指令優化),也都是不夠執行緒安全的。

那麼怎麼辦呢?併發程式設計的藝術上,對該問題提出了ABA的模型。以及對應的解決正規化。具體本文不詳細寫。需要的同學私信獲取答案。

為什麼ABA的模型可以解決併發情況下多執行緒的資料安全性問題呢?是因為它避免了在彙編過程中進行指令優化時帶來的執行順序的異常。

上面講了併發性的資料安全性問題。

那麼是否高併發程式設計解決了併發的多執行緒的資料安全性問題是否就解決了高併發的問題呢?

答案還是 不是。

高併發除了資料安全性問題。還有一個層面的問題:資源瓶頸。

這又是為什麼呢?當一個使用者登陸的時候,讀取的是一個使用者的資訊到記憶體,進行比較。

假設記憶體1g,一個使用者資訊1M,那麼在記憶體裡面最多同時可以有使用者資訊1024條(不考慮其他程式佔用記憶體的情況)。假設登陸在一萬個使用者來的時候,對資料庫的壓力導致訪問時間延長為1s,那麼在一秒的時間內,這個1g記憶體只能供給1024個使用者進行登陸。來了一萬個使用者,就需要將近10s的時間,記憶體才會完全的處理完畢登陸操作。因此,如果其他資源夠用的情況下,超過1024個使用者同時登陸,必然會出現記憶體是瓶頸的問題。(可以按照該方式進行計算記憶體的負載能力)。那實際情況下,並非完全如此。因為當記憶體使用量達到一定比例的時候,可能會觸發與硬碟的快取的互動(涉及到排程演算法)。如果在高位頻繁出現或持續在高位導致頻繁排程,就會對cpu造成壓力(排程演算法是計算密集型任務,比較耗cpu資源),嚴重可能直接導致cpu使用率100%出現當機的狀況。這個時候,資源瓶頸就轉換到cpu。

再換個場景,比如是下載資源或者上傳資原始檔的介面,這個很好理解,當很多使用者同時上傳的時候,網路頻寬有限,會擠爆網路,導致上傳失敗或者下載失敗。這個情況下高併發的資源瓶頸在網路。

其他硬體都可能因為高併發的功能不一樣導致資源瓶頸。那我們就說,高併發實際上也是資源瓶頸問題。

如果我們硬體很給力,完全夠用,是否高併發問題就好了呢?

這裡其實還有一個層面:軟體程式編寫。

如果程式在多次呼叫不釋放資源的情況下,也是會造成雖然訪問不是在同一時刻,仍然可能出現資源耗盡的問題。那可能就是上個時間段佔用的資源(例如map佔用了記憶體)在很長時間內無法釋放。導致雖然不是同一時刻訪問的多個執行緒,也會出現資源耗盡的情況。這也算是高併發的一個方面。所以,寫程式碼的時候,要對程式碼質量進行把控,也有個詞,叫冪等性。

基於以上講解,高併發程式設計實際上主要解決以上幾個方面:

1.共享資料的安全性問題

2.共享資源的瓶頸問題

3.共享資源的使用性問題

解決了以上三個大問題,併發程式設計和其他的程式設計,也就不存在考慮不到的死角問題了。高併發並不可怕,掌握以上三大方面,高併發也只不過是找到一個上限值的問題了。

相關文章