日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

并发调试和JDK8新特性

發布時間:2024/4/13 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 并发调试和JDK8新特性 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
第一個是多線程的調試方法,第二個是關于dump的分析,我們在系統出問題的時候,可以看有哪些線程在運行,每個線程的堆棧是什么情況,如果我們發現有些線程被卡死了,我們從堆棧的分析當中呢,往往可以得到一些有價值的信息,知道這個線程為什么卡到這里不動,我們介紹JDK8當中,對并發產生的一些新的支持,我們對內置的并發包當中呢,線程池,ForkJoinPool,這些都是涵蓋在JDK567當中的,JDK8他比較新,它會提供一些性能更加好的,使用更加方便的并發類我們也會在這個課程中做一個介紹,首先我們來看一下多線程的調試

多線程調試呢和單線程相比呢,困難程度會有所增加,原因就是我們同樣的代碼,他可能被多個線程一起執行,多線程并行的運行的先后順序呢,還有些不確定的,有些時候線程會先執行,你在同樣運行同樣的程序,可能會有完全不同的輸出,并不像單線程的,串行程序一樣,你只要起始條件一樣,初始條件一樣,那你的運行結果基本上都是一樣的,基本上起始和終止完全是一樣的,也就是你所有的內容都是可以重現的,但是并行程序并不能保證,他的結果是可以百分百可以重現的,有些問題有時候可以重現,有些時候不能重現,這種很有可能是多線程造成的,因此我們也需要用到對多線程調試的方法,手動的在調試過程中哪個線程誰先執行誰后執行,執行到什么程度的時候,哪個線程執行到哪個階段,另外線程再切入進來,這里有一個簡單的例子,就是我們的ArrayList,我們ArrayList他不是線程安全的,這樣的代碼他一定是錯誤的,我們有兩個線程去執行這個東西,這個程序一定是錯的

add方法出了問題,ArrayList當時是為了實現一個無鎖的Vector,所以也介紹了Vector的實現原理,大體上應該都是相當的,因為內部實現是一個數組,當數組容量不夠的時候呢,進行擴容,因此在加入一個元素之前呢,我當前的數組的容量是不是夠的,如果不夠我就進行擴容

當這個條件成立的時候才會生效,這個條件斷點的使用呢,在各種場合下都可以使用

現在我們再來看一下線程dump的分析,我們可以使用jstack的工具,找出正在虛擬機下面的,所有的線程,然后我們注意看,每個線程當前正在做些什么事情,我們可以推測說,如果出問題的話,這個問題出在哪里,舉個例子比如說,你發現一個程序,他卡死了,那你可以找出來這個程序在干什么,他執行到哪行代碼上上面,卡死了,他等待在某個鎖上面,某個對象上面,這個鎖或對象唄占用了,在旁邊等待,你可以通過dump出來的東西呢,你可以看到當前線程有哪些問題,還有些情況呢,發現是卡住了,每次都是在同樣的一段代碼上面,但是也沒有等待在某個鎖上面,線程就陷入到一個死循環上面去了,這個也是有遇到過的,但是沒有在任何鎖上做等待,這個多半就有可能是死循環

JDK8當中對多線程的一個新的支持,首先看LongAdder這么一個類,累加器,它是非常接近AtomicInteger,它是一個long型的,他更加接近AtomicLong,Atomic長整形,他的性能在高并發的情況下呢,要比AtomicLong要更好,AtomicLong已經要比鎖要好很多,它是一個無鎖的操作,為什么LongAdder要比原子操作性能要更好呢,其實LongAdder里面也是使用原子操作,他用了一些額外的處理技術,熱點分離,這個我們在計算ConcurrentHashMap的時候呢,介紹過這種思想,一個大的HashMap,如果給大的對象加鎖,性能會比較差,如果我們把它分成16個小HashMap,每次只操作其中一個HashMap,我們只拿他的十六分之一進行加鎖,這樣沖突的可能性就會減少,如果沖突減少了,性能自然就提高了,并不是無鎖阻塞才需要做熱點分離,才要做這個沖突的避免,無鎖操作我也要沖突,如果我一個線程不斷嘗試失敗,那我還是要做這個循環,知道嘗試成功為止,所以如果我把熱點數據進行分離之后呢,減少這個沖突,獲取每次操作的成功率呢,會大大上升,只要我操作成功上升呢,我這個性能也能得到提高,本來我要兩次循環,現在我一次循環就能夠做好,那我這個性能就提高了一倍,和原子類也比較像,add是原來上增加多少,increment增加1,decrement就是add -1,sum和longValue是一樣的意思,剛才有說過,因為LongAdder內部也做了熱點分離,他把一個整數分解為若干個整數,最終的結果就是若干個整數,數據的求和,這兩個其實是等價的,這里就是把long轉成一個整形,這里就是LongAdder的一些使用

內部LongAdder他會做一個什么事情呢,它會把原來作為一個整數的,數字分解為一個數組,分解的原因呢,數組分解完之后呢,有一個cells,每一個小單元呢,都是一個整數,當你一個線程進來的時候,做CAS循環的操作,因此失敗率就會比較高,在高并發的時候,但是如果說,多個線程他對應的是不同的單元格,最后LongAdder他所有的值呢,是所有的cells相加得來的,這只是一個累加器,那這種情況呢,打散之后呢,他們的沖突概率就減小,這樣CAS更新的成功率就提高,性能就會增加,但是換句話說,如果當前的數據競爭并不是很厲害,并沒有多少線程在參與這個事情,可能就是一兩個線程在做這個操作,如果像這種情況,你也去分出多個單元格來,然后再做累加求和,這樣也是有點浪費資源,因為你完全用一個數字就可以解決問題了,我真的有很多線程在做這個操作的時候,我這樣做是有意義的,LongAdder他本身也考慮到了這種情況,他也知道說,并不是所有的LongAdder在高并發的操作下,有時候我可能就是一個線程或者兩個線程,因此它并不是無條件的把數據打散到不同的cell上面去,他在正常執行的時候呢,內部也會維護一個base的元素,就是原子的long型的數據,但是每次你去做操作的時候呢,它會對這個數據進行一個累加,加或者減,但是他一旦發現沖突,他只要發現又一次沖突之后呢,他就會去創建這個數組,創建這個cells,cell數組在最初的時候,LongAdder剛剛創建的時候,是并不存在的,數組當中的元素也不會太多,也就兩個,每當發現cell操作有沖突,他就會對這個cell做擴展,不停的擴展,只要他沒有發現沖突,就不會做這個擴展,但是他發現沖突,才會做這個擴展,因此在最終這個場合下面,他很有可能就永遠不發生沖突,這個就是LongAdder的基本思想,可以說有點自適應的感覺,不停的把容量給擴大,避免沖突的產生,內部大概就是使用這么一種策略,當你求和的時候,你把所有的部分都要加起來

下面我們來看JDK8當中另外的一個類,方便大家使用的一個工具類,CompletableFuture,完成的Future接口,他實現的是CompletableStage接口,這個接口大概有40個方法,單純從面向對象的角度來說,我們接口方法還是少一點,為什么接口當中有40個方法呢,因為我們在JDK8當中,流式的API,一個套一個的去寫,這個在后面會做一個簡單的介紹,函數編程當中我們看到了這個東西,本質上就是一個匿名的內部類,相當于在內部實現了一個Runnable的方法,CompletableFuture呢,也就是對我們以前的Future模式,他的一個增強版

這里是一個簡單的實現,一個簡單的應用,我們使用Future模式的一個功能呢,可以提交我們的請求,然后拿到Future,類似訂單的一個東西,如果我們后臺任務沒有完成,那我們去get這個結果的時候呢,會進行一個等待,當后臺數據完成之后,我們再去get他,我們就可以馬上得到這個信息,這是future的一個基本的功能,對于CompletableFuture來講,他把完成動作的這個功能呢,開放給我,比如在這個地方,我們可以新建一個CompletableFuture,一個可完成的Future,然后我們會把Future傳給AskThread線程,這個Ask線程會做真什么事情呢,做一個平方然后返回,要有數據才能做平方,如果沒有數據get肯定就是一個阻塞,那你什么時候Future能夠完成呢,對于CompletableFuture來講,完成Future的時間點,我們在Future當中就有一個complete方法,來告知完成,你想他什么時候完成,他就什么時候完成,當你運行這個方法之后,這個地方,他就能得到通知說,前面的Future已經完成了,繼續往下走,他把完成操作這個功能,時間點開放出來,因此來講是一個更加靈活的Future

他可以做異步的執行,和普通的future比較的接近,CompletableFuture有一個supplyAsync,他要求把一個函數,模擬一個比較長時間的執行,在這把一個東西去執行它,它是一個工廠方法,他并不是讓你去new一個CompletableFuture出來,而是內部去創建一個對象實例,然后我們可以得到他的結果,這個跟一般的使用方法比較接近

說白了就是線程池,線程池當中去執行它,run和supply有什么區別呢,沒有返回值的,run是單純的runnable接口,他是沒有返回值的

CompletableFuture他可以通過工廠方法呢,把對象實例創建出來之后,我們可以在這個實例之上,再進行流式結果的處理,建立一個平方,我們可以對結果進一步處理,把這個結果轉成字符串,一連串的操作可以在一個語句里完成,有一種函數式編程的傾向

把另外一個對象實例進行組裝,這個就是CompletableFuture的一個功能,為了支持函數式編程,原本Future模式,讓你決定什么時候來完成通知,就這個類而言,跟性能本身是沒有關系的,更多的是方便性的一個操作,其實是可以壓縮編碼量的

最后來看一下StampedLock這個東西,前面我們有說過讀寫鎖,讀寫鎖要比單純鎖的功能要好很多,讀寫鎖不會讀和讀之間的競爭,他認為所有的讀都是可以并發的,只有當讀和讀,寫和寫的時候呢,他才會等待,讀很多的情況之下,讀寫鎖對性能的提升是有幫助,但是StampedLock在讀寫上面呢,他又更近了一步,他認為說,對于讀來講,如果我讀也堵塞了寫,他認為不是一件很好的事情,他認為我讀也不應該堵塞寫,而是說我在讀的時候,發生了寫,那我讀要做重讀,而不是說讀不讓寫去操作,那這樣的思路有什么好處呢,但有大量的讀線程存在,其實寫線程是有可能發生饑餓現象,因為你有太多的讀,寫線程不能得到及時的調度,你寫不進去,因為讀太多了,而你采用StampedLock,他寫不會阻塞讀,讀很容易拿到讀鎖,而寫的時候很容易拿到寫鎖,然后進行寫,因為你寫的時候很容易破壞數據,沒有寫完的時候數據是不一致的,當讀線程發現數據不一致的時候,他其實是可以做一個重讀的操作,從這個角度上來說,當你讀寫線程在混用的時候,讀和寫是不會阻塞對方,這里有一個例子,點有x和y,這里我們就是用StampedLock,Stamped就是時間戳的意思,更像是一個時間戳,類似一個郵戳,你每次做寫操作的時候呢,其實我們都可以對時間戳進行加1,內部實現可能不是加1,累加上去之后呢,我郵戳這個值,不停的變化,我可以通過這個變化,來判斷當前這個鎖,或者當前這個數據,究竟有沒有被人占用,對于讀來講呢,他支持讀鎖,但是他這里會有一個新的樂觀的讀,就是tryOptimisticRead,就是樂觀讀,樂觀讀就是采用我們剛才的思想,他不會阻塞寫,樂觀讀讀到Stamped的時候,就相當于我拿到了郵戳是多少,然后我把x,y讀出來,就是類當中的x,y,因為我在讀x的時候呢,y肯定被人改掉了,因此我最終讀到的x,y,currentX,currentY,并不一定是一個一致的數據,那有可能就是不一致的,因為你在讀x的時候是好的,y被人改了,y可能就是另外一個點的數據,因此并不一定是一致的,全部讀完之后,我要去做一個驗證,我驗證剛剛拿到的讀寫鎖,樂觀讀的stamped,到底是不是可用的,validate成功的前提是什么呢,條件是什么呢,我在拿樂觀讀的時候,進行樂觀讀的時候,寫鎖并沒有被占用,如果當前的寫鎖是被占用的,樂觀鎖一定要宣告失敗,就相當于我樂觀鎖就返回一個0,0一定是一個失敗的樂觀讀,因為如果當前數據正在寫,假設這個地方是一個合理的數據,但是我要驗證Stamped的時候,你當前的stamped,StampedLock存在一個類似時間戳的東西,你每做一個writeLock,時間戳都會變化,都相當于加1,當你每unlockWrite,他也會去發生變化,如果沒有發生溢出,它會重復的,每次加鎖和解鎖的時候,stamped是會發生變化的,因此我剛才拿到的stamped的值,是不是和內部的sl的值是不是一樣的,如果是一樣的呢,說明在整個讀的過程當中,并沒有人去加鎖,并且我在try樂觀鎖的時候,也沒有人去加鎖,那我就可以認為,樂觀讀是成功的,只要我樂觀讀成功,我就要做我要做的事情,比如我這里是計算,點到原點的距離,因為我這個讀是成功的,我就可以直接把他給算出來,currentX和currentY是合理的,是一致的,當我這個地方驗證不成功,也就是我在執行這句話的時候,可能有人加了writeLock,這個時候我stamped的值,內部保存stamped的值,他不一致,那我就認為你驗證失敗,驗證失敗其實有好幾種處理方法,一種是我驗證失敗我再去驗證一次,再去加一次樂觀鎖,就是我們CAS操作的這種思路,就是我寫一個死循環,就是不停的拿樂觀鎖,知道我成功為止,這個地方演示了readLock的使用,那么我就放棄了樂觀鎖,我就使用悲觀的策略,這就相當于是讀鎖,然后去做數據的讀,然后就釋放讀鎖,這種情況就是一種悲觀的讀法,StampedLock提供了樂觀讀的思路

他也會使用CLH自旋的策略,這個自旋是什么意思呢,如果我們發現讀失敗的情況,他不會立即把這個線程掛起,鎖當中會維護一個隊列,所有申請鎖,但是沒有成功的線程都記錄在這個隊列中,每一個節點會包含一個信息,共同組成一個鏈表,它會保存Lock的一個標記位,前面的線程是不是持有了這個鎖,判斷當前線程是否已經釋放了這個鎖,當一個線程試圖獲得鎖的時候呢,它會取得當前等待隊列尾部結點作為其前節點,并且使用下面類似方法進行加鎖判斷,前面節點的鎖有沒有釋放掉,如果前面節點的鎖有釋放掉呢,那我就可以記錄這個事情,否則我就應該繼續循環等待,這個就是一個死循環,這個死循環其實就是不停的等待,前面節點去釋放這個鎖,但是當前線程本身呢,在這個循環當中,所以他是不會被操作系統掛起的,這個就是自旋鎖的一個基本思想,他把所有的拿不到鎖的,想要鎖的線程呢,保留起來,不斷地做這個循環

每一個線程看前面的線程是不是已經釋放了鎖,一旦發現釋放了就開始執行,開始往后執行,但是StampedLock不會無休止的執行,如果所有的線程拿不到鎖,無休止的輪詢,那肯定是不行的,那CPU的占用率是很高,所以這里只是一段示意代碼,會做自旋,但是無限制的自旋,他的自旋是有一定的次數的,超過某一個次數之后,比如超過1千次之后,他就會把這個線程進行park操作,就是把它作為一個等待操作,否則就會做若干個自旋,這個也是StampedLock內部實現的機制

?

總結

以上是生活随笔為你收集整理的并发调试和JDK8新特性的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。