什麼是高併發(java爲例)

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

以登錄功能爲例。當登錄的時候,是用戶拿用戶名,密碼到數據庫裏訪問是否存在,存在則跳轉到登錄頁面。然後修改訪問次數爲+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.共享資源的使用性問題

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

 

 

 

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