十分鐘帶你深入了解多線程——多線程關于鎖的優化(一)

      網友投稿 1022 2025-04-02

      一、有助于提高鎖性能的幾點建議

      鎖的競爭必然會導致程序的整體性能下降。為了將這種副作用降到最低,這里提出一些關于使用鎖的建議,希望可以幫助大家寫出性能更高的程序。

      1、減少鎖持有時間

      對于使用鎖進行并發控制的應用程序而言,在鎖競爭過程中,單個線程對鎖的持有時間與系統性能有著直接的關系。如果線程持有鎖的時間越長,那么相對地,鎖的競爭程度也就越激烈。可以想象一下,如果要求100 個人各自填寫自己的身份信息,但是只給他們一支筆,那么如果每個人拿著筆的時間都很長,總體所花的時間就會很長。如果真的只有一支筆共享給100個人用,那么最好讓每個人花盡量少的時間持筆,務必做到想好了再拿筆寫,千萬不能拿著筆才去思考這表格應該怎么填。程序開發也是類似的,應該盡可能地減少對某個鎖的占有時間,以減少線程間互斥的可能。以下面的代碼段為例:

      public synchronized void syncMethod(){ othercode1(); mutextMethod(); othercode2(); }

      在syncMethod()方法中,假設只有mutextMethod()方法是有同步需要的,而othercode1(方法和othercode2()方法并不需要做同步控制。如果 othercode1()和 othercode2()分別是重量級的方法,則會花費較長的CPU時間。如果在并發量較大時,使用這種對整個方法做同步的方案,則會導致等待線程大量增加。因為一個線程,在進入該方法時獲得內部鎖,只有在所有任務都執行完后,才會釋放鎖。

      一個較為優化的解決方案是,只在必要時進行同步,這樣就能明顯減少線程持有鎖的時間,提高系統的吞吐量。

      public void syncMethod2 (){ othercode1(); synchronized (this){ mutextMethod (); } othercode2(); }

      在改進的代碼中只針對mutextMethod()方法做了同步,鎖占用的時間相對較短,因此能有更高的并行度。這種技術手段在JDK的源碼包中也可以很容易地找到,比如處理正則表達式的Pattern類。

      public Matcher matcher(CharSequence input) { if (!compiled){ synchronized (this){ if (!compiled) compile(); } } Matcher m= new Matcher (this, input); return m; }

      matcher()方法有條件地進行鎖申請,只有在表達式未編譯時,進行局部的加鎖。這種處理方式大大提高了matcher(方法的執行效率和可靠性。

      注意:減少鎖的持有時間有助于降低鎖沖突的可能性,進而提升系統的并發能力。

      2、減小鎖粒度

      減小鎖粒度也是一種削弱多線程鎖競爭的有效手段。這種技術典型的使用場景就是ConcurrentHashMap類的實現。

      對于HashMap來說,最重要的兩個方法就是get()和 put()。一種最自然的想法就是,對整個HashMap加鎖從而得到一個線程安全的對象,但是這樣做,加鎖粒度太大。對于ConcurrentHashMap類,它內部進一步細分了若干個小的HashMap,稱之為段(SEGMENT)。在默認情況下,一個ConcurrentHashMap類可以被細分為16個段。

      如果需要在ConcurrentHashMap類中增加一個新的表項,并不是將整個HashMap加鎖,而是首先根據hashcode得到該表項應該被存放到哪個段中,然后對該段加鎖,并完成put()方法操作。在多線程環境中,如果多個線程同時進行 put()方法操作,只要被加入的表項不存放在同一個段中,線程間便可以做到真正的并行。

      由于默認有16個段,因此,如果夠幸運的話,ConcurrentHashMap類可以接受16個線程同時插入(如果都插入不同的段中),從而大大提升其吞吐量。下面代碼顯示了 put()方法操作的過程。第5~6行代碼根據key 獲得對應段的序號。接著在第9行得到段,然后將數據插入給定的段中。

      public v put(K key,v value) { Segment S; if (value -= null) throw new NullPointerException(); int hash = hash (key); int j =(hash >>> segmentShift) & segmentMask; if((s= (Segment)UNSAFE.getObject (segments,(j<

      但是,減小鎖粒度會帶來一個新的問題,即當系統需要取得全局鎖時,其消耗的資源會比較多。仍然以ConcurrentHashMap類為例,雖然其 put()方法很好地分離了鎖,但是當試圖訪問ConcurrentHashMap類的全局信息時,就需要同時取得所有段的鎖方能順利實施。比如ConcurrentHashMap類的size()方法,它將返回ConcurrentHashMap類的有效表項的數量,即 ConcurrentHashMap類的全部有效表項之和。要獲取這個信息需要取得所有子段的鎖,因此,其 size()方法的部分代碼如下:

      sum= 0; for (int i = 0; i

      可以看到在計算總數時,先要獲得所有段的鎖再求和。但是,ConcurrentHashMap類的size()方法并不總是這樣執行的,事實上,size()方法會先使用無鎖的方式求和,如果失敗才會嘗試這種加鎖的方法。但不管怎么說,在高并發場合ConcurrentHashMap類的size()方法的性能依然要差于同步的HashMap。

      因此,只有在類似于 size()方法獲取全局信息的方法調用并不頻繁時,這種減小鎖粒度的方法才能在真正意義上提高系統的吞吐量。

      注意:所謂減小鎖粒度,就是指縮小鎖定對象的范圍,從而降低鎖沖突的可能性,進而提高系統的并發能力。

      3、用讀寫分離鎖來代替獨占鎖

      之前我們已經提過,使用讀寫分離鎖ReadWriteLock可以提高系統的性能。使用讀寫分離鎖來替代獨占鎖是減小鎖粒度的一種特殊情況。如果說減小鎖粒度是通過分割數據結構實現的,那么讀寫分離鎖則是對系統功能點的分割。

      在讀多寫少的場合,讀寫鎖對系統性能是很有好處的。因為如果系統在讀寫數據時均只使用獨占鎖,那么讀操作和寫操作間、讀操作和讀操作間、寫操作和寫操作間均不能做到真正的并發,并且需要相互等待。而讀操作本身不會影響數據的完整性和一致性。因此,從理論上講,在大部分情況下,可以允許多線程同時讀,讀寫鎖正是實現了這種功能。由于我們在第3章中已經介紹了讀寫鎖,因此這里就不再重復了。

      注意:在讀多寫少的場合使用讀寫鎖可以有效提升系統的并發能力。

      4、鎖分離

      如果將讀寫鎖的思想進一步延伸,就是鎖分離。讀寫鎖根據讀寫操作功能上的不同,進行了有效的鎖分離。依據應用程序的功能特點,使用類似的分離思想,也可以對獨占鎖進行分離。一個典型的案例就是java.util.concurrent.LinkedBlockingQueue的實現。

      在LinkedBlockingQueue 的實現中,take()函數和 put()函數分別實現了從隊列中取得數據和往隊列中增加數據的功能。雖然兩個函數都對當前隊列進行了修改操作,但由于LinkedBlockingQueue是基于鏈表的,因此兩個操作分別作用于隊列的前端和尾端,從理論上說,兩者并不沖突。

      如果使用獨占鎖,則要求在兩個操作進行時獲取當前隊列的獨占鎖,那么take()方法和put()方法就不可能真正的并發,在運行時,它們會彼此等待對方釋放鎖資源。在這種情況下,鎖競爭會相對比較激烈,從而影響程序在高并發時的性能。

      因此,在JDK的實現中,并沒有采用這樣的方式,取而代之的是用兩把不同的鎖分離了take()方法和 put)方法的操作。

      十分鐘帶你深入了解多線程——多線程關于鎖的優化(一)

      /** Lock held by take, poll, etc */ private final ReentrantLock takeLock = new ReentrantLock();//take()方法需要持有takeLock/**ait queue for waiting takes * / private final Condition notEmpty - takeLock.newCondition ();/** Lock held by put,offer,etc*/ private final ReentrantLock putLock = new ReentrantLock();//put()方法需要持有putLock./**wait queue for waiting puts */ private final Condition notFull= putLock.newCondition();

      以上代碼片段定義了takeLock和 putLock,它們分別在 take()方法和 put()方法中使用。因此,take()方法和 put()方法就此相互獨立,它們之間不存在鎖競爭關系,只需要在take()方法和 take()方法間、put()方法和put()方法間分別對takeLock和 putLock進行競爭。從而,削弱了鎖競爭的可能性。

      take()方法的實現如下,筆者在代碼中給出了詳細的注釋,故不在正文中做進一步說明了。

      public E take()throws InterruptedException{ Ex; int c - -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock;takeLock. lockInterruptibly(); //不能有兩個線程同時取數據 try { try { while (count.get( =0) //如果當前沒有可用數據,則一直等待 notEmpty.await (); //等待put()方法操作的通知 ] catch (InterruptedException ie){ notEmpty.signal (); //通知其他未中斷的線程 throw ie; x=extract(); //取得第一個數據 C= count.getAndDecrement (O; //數量減1,原子操作,因為會和put ()//函數同時訪問count。注意:變量c是//count減1前的值 if(c >1) notEmpty.signal (); //通知其他take()方法操作 } finally { takeLock.unlock(); //釋放鎖 if(c -= capacity) signalNotFul1(0); //通知put()方法操作,已有空余空間 return x;

      函數put()的實現如下。

      public void put(Ee) throws InterruptedException{ if (e -= null)throw new NullPointerException();int c= -1; final ReentrantLock putLock = this.putLock;final AtomicInteger count = this.count;putLock.lockInterruptiblyO; //不能有兩個線程同時進行put()方法 try { try { while (count.get( -=capacity) //如果隊列已經滿了 notFull.await(; //等待 }catch (InterruptedException ie) { notFull.signal(); //通知未中斷的線程 throw ie; insert(e); //插入數據 C=count.getAndIncrement (); //更新總數,變量c是count加1前的值 if (c+1< capacity) notFull.signal (); //有足夠的空間,通知其他線程 }finally{ putLock.unlock(); //釋放鎖 if (c ==0) signalNotEmpty();//插入成功后,通知take ()方法取數據 }

      通過takeLock和putLock 兩把鎖,LinkedBlockingQueue實現了取數據和寫數據的分離,使兩者在真正意義上成為可并發的操作。

      4、鎖粗化

      通常情況下,為了保證多線程間的有效并發,會要求每個線程持有鎖的時間盡量短,即在使用完公共資源后,應該立即釋放鎖。只有這樣,等待在這個鎖上的其他線程才能盡早地獲得資源執行任務。但是,凡事都有一個度,如果對同一個鎖不停地進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利于性能的優化。

      為此,虛擬機在遇到一連串連續地對同一個鎖不斷進行請求和釋放的操作時,便會把所有的鎖操作整合成對鎖的一次請求,從而減少對鎖的請求同步的次數,這個操作叫作鎖的粗化。

      public void demoMethod () { synchronized(lock){ //do sth. ) //做其他不需要的同步的工作,但能很快執行完畢synchronized(lock){ //do sth.

      上面的代碼段會被整合成如下形式;

      public void demoMethod({ //整合成一次鎖請求synchronized (lock){ //do sth. //做其他不需要的同步的工作,但能很快執行完畢 )

      在開發過程中,大家也應該有意識地在合理的場合進行鎖的粗化,尤其當在循環內請求鎖時。以下是一個循環內請求鎖的例子,在這種情況下,意味著每次循環都有申請鎖和釋放鎖的操作。但在這種情況下,顯然是沒有必要的。

      for(int i=0;i

      所以,一種更加合理的做法應該是在外層只請求一次鎖:

      synchronized (lock){ for(int i=0;i

      注意:性能優化就是根據運行時的真實情況對各個資源點進行權衡折中的過程。鎖粗化的思想和減少鎖持有時間是相反的,但在不同的場合,它們的效果并不相同,因此要根據實際情況進行權衡。

      摘自JAVA高并發程序設計,推薦推薦

      任務調度 多線程

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:excel輸入內容不符合限制條件怎么辦(為什么excel輸入內容,不符合限制條件)
      下一篇:非常實用且鮮為人知的Excel技巧
      相關文章
      亚洲电影免费在线观看| 区三区激情福利综合中文字幕在线一区亚洲视频1 | 亚洲综合无码无在线观看| 日韩亚洲Av人人夜夜澡人人爽| 亚洲国产精品无码久久久不卡| 亚洲中文久久精品无码| 中文字幕亚洲日韩无线码| 亚洲成AV人在线观看网址| 亚洲第一永久AV网站久久精品男人的天堂AV| 亚洲欧美成aⅴ人在线观看| 亚洲依依成人亚洲社区| 亚洲一卡2卡三卡4卡无卡下载| 亚洲AV男人的天堂在线观看| 激情内射亚洲一区二区三区爱妻| 中文字幕亚洲男人的天堂网络| 中文字幕亚洲男人的天堂网络 | 亚洲一欧洲中文字幕在线| 亚洲成a人片在线观看中文app | 亚洲美女在线观看播放| 亚洲国产综合人成综合网站00| 亚洲另类精品xxxx人妖| 亚洲一区二区三区高清视频| 亚洲日本久久久午夜精品| 亚洲欧美乱色情图片| 老牛精品亚洲成av人片| 偷自拍亚洲视频在线观看| 亚洲精品乱码久久久久久不卡| ZZIJZZIJ亚洲日本少妇JIZJIZ | 亚洲国产成人久久77| 亚洲人成网站看在线播放| 亚洲夂夂婷婷色拍WW47| 亚洲国产欧美国产综合一区 | 亚洲精品在线不卡| 亚洲乱码中文论理电影| 亚洲中文字幕AV每天更新| 亚洲JLZZJLZZ少妇| 亚洲综合激情另类专区| 亚洲国产日韩在线视频| 亚洲av无码不卡一区二区三区| 亚洲综合一区二区国产精品| 亚洲人成高清在线播放|