java 优化线程_Java | 多线程调优(下):如何优化多线程上下文切换?
通過上一講的講解,相信你對上下文切換已經有了一定的了解了。如果是單個線程,在 CPU 調用之后,那么它基本上是不會被調度出去的。如果可運行的線程數遠大于 CPU 數量,那么操作系統最終會將某個正在運行的線程調度出來,從而使其它線程能夠使用 CPU ,這就會導致上下文切換。
還有,在多線程中如果使用了競爭鎖,當線程由于等待競爭鎖而被阻塞時,JVM 通常會將這個鎖掛起,并允許它被交換出去。如果頻繁地發生阻塞,CPU 密集型的程序就會發生更多的上下文切換。
那么問題來了,我們知道在某些場景下使用多線程是非常必要的,但多線程編程給系統帶來了上下文切換,從而增加的性能開銷也是實打實存在的。那么我們該如何優化多線程上下文切換呢?這就是我今天要和你分享的話題,我將重點介紹幾種常見的優化方法。
競爭鎖優化
大多數人在多線程編程中碰到性能問題,第一反應多是想到了鎖。
多線程對鎖資源的競爭會引起上下文切換,還有鎖競爭導致的線程阻塞越多,上下文切換就越頻繁,系統的性能開銷也就越大。由此可見,在多線程編程中,鎖其實不是性能開銷的根源,競爭鎖才是。
第 11~13 講中我曾集中講過鎖優化,我們知道鎖的優化歸根結底就是減少競爭。這講中我們就再來總結下鎖優化的一些方式。
1. 減少鎖的持有時間
我們知道,鎖的持有時間越長,就意味著有越多的線程在等待該競爭資源釋放。如果是 Synchronized 同步鎖資源,就不僅是帶來線程間的上下文切換,還有可能會增加進程間的上下文切換。
在第 12 講中,我曾分享過一些更具體的方法,例如,可以將一些與鎖無關的代碼移出同步代碼塊,尤其是那些開銷較大的操作以及可能被阻塞的操作。
優化前
public?synchronized?void?mySyncMethod(){
businesscode1();
mutextMethod();
businesscode2();
}
優化后
public?void?mySyncMethod(){
businesscode1();
synchronized(this)
{
mutextMethod();
}
businesscode2();
}
2. 降低鎖的粒度
同步鎖可以保證對象的原子性,我們可以考慮將鎖粒度拆分得更小一些,以此避免所有線程對一個鎖資源的競爭過于激烈。具體方式有以下兩種:
與傳統鎖不同的是,讀寫鎖實現了鎖分離,也就是說讀寫鎖是由“讀鎖”和“寫鎖”兩個鎖實現的,其規則是可以共享讀,但只有一個寫。
這樣做的好處是,在多線程讀的時候,讀讀是不互斥的,讀寫是互斥的,寫寫是互斥的。而傳統的獨占鎖在沒有區分讀寫鎖的時候,讀寫操作一般是:讀讀互斥、讀寫互斥、寫寫互斥。所以在讀遠大于寫的多線程場景中,鎖分離避免了在高并發讀情況下的資源競爭,從而避免了上下文切換。
我們在使用鎖來保證集合或者大對象原子性時,可以考慮將鎖對象進一步分解。例如,我之前講過的 Java1.8 之前版本的 ConcurrentHashMap 就使用了鎖分段。
3. 非阻塞樂觀鎖替代競爭鎖
volatile 關鍵字的作用是保障可見性及有序性,volatile 的讀寫操作不會導致上下文切換,因此開銷比較小。但是,volatile 不能保證操作變量的原子性,因為沒有鎖的排他性。
而 CAS 是一個原子的 if-then-act 操作,CAS 是一個無鎖算法實現,保障了對一個共享變量讀寫操作的一致性。CAS 操作中有 3 個操作數,內存值 V、舊的預期值 A 和要修改的新值 B,當且僅當 A 和 V 相同時,將 V 修改為 B,否則什么都不做,CAS 算法將不會導致上下文切換。Java 的 Atomic 包就使用了 CAS 算法來更新數據,就不需要額外加鎖。
上面我們了解了如何從編碼層面去優化競爭鎖,那么除此之外,JVM 內部其實也對 Synchronized 同步鎖做了優化,我在 12 講中有詳細地講解過,這里簡單回顧一下。
在 JDK1.6 中,JVM 將 Synchronized 同步鎖分為了偏向鎖、輕量級鎖、自旋鎖以及重量級鎖,優化路徑也是按照以上順序進行。JIT 編譯器在動態編譯同步塊的時候,也會通過鎖消除、鎖粗化的方式來優化該同步鎖。
wait/notify 優化
在 Java 中,我們可以通過配合調用 Object 對象的 wait() 方法和 notify() 方法或 notifyAll() 方法來實現線程間的通信。
在線程中調用 wait() 方法,將阻塞等待其它線程的通知(其它線程調用 notify() 方法或 notifyAll() 方法),在線程中調用 notify() 方法或 notifyAll() 方法,將通知其它線程從 wait() 方法處返回。
下面我們通過 wait() / notify() 來實現一個簡單的生產者和消費者的案例,代碼如下:
public?class?WaitNotifyTest?{
public?static?void?main(String[]?args)?{
Vectorpool=new?Vector();
Producer?producer=new?Producer(pool,?10);
Consumer?consumer=new?Consumer(pool);
new?Thread(producer).start();
new?Thread(consumer).start();
}
}
/**
*?生產者
*?@author?admin
*
*/
class?Producer?implements?Runnable{
private?Vectorpool;
private?Integer?size;
public?Producer(Vectorpool,?Integer?size)?{
this.pool?=?pool;
this.size?=?size;
}
public?void?run()?{
for(;;){
try?{
System.out.println("生產一個商品?");
produce(1);
}?catch?(InterruptedException?e)?{
//?TODO?Auto-generated?catch?block
e.printStackTrace();
}
}
}
private?void?produce(int?i)?throws?InterruptedException{
while(pool.size()==size){
synchronized?(pool)?{
System.out.println("生產者等待消費者消費商品,當前商品數量為"+pool.size());
pool.wait();//等待消費者消費
}
}
synchronized?(pool)?{
pool.add(i);
pool.notifyAll();//生產成功,通知消費者消費
}
}
}
/**
*?消費者
*?@author?admin
*
*/
class?Consumer?implements?Runnable{
private?Vectorpool;
public?Consumer(Vectorpool)?{
this.pool?=?pool;
}
public?void?run()?{
for(;;){
try?{
System.out.println("消費一個商品");
consume();
}?catch?(InterruptedException?e)?{
//?TODO?Auto-generated?catch?block
e.printStackTrace();
}
}
}
private?void?consume()?throws?InterruptedException{
synchronized?(pool)?{
while(pool.isEmpty())?{
System.out.println("消費者等待生產者生產商品,當前商品數量為"+pool.size());
pool.wait();//等待生產者生產商品
}
}
synchronized?(pool)?{
pool.remove(0);
pool.notifyAll();//通知生產者生產商品
}
}
}
wait/notify 的使用導致了較多的上下文切換
結合以下圖片,我們可以看到,在消費者第一次申請到鎖之前,發現沒有商品消費,此時會執行 Object.wait() 方法,這里會導致線程掛起,進入阻塞狀態,這里為一次上下文切換。
當生產者獲取到鎖并執行 notifyAll() 之后,會喚醒處于阻塞狀態的消費者線程,此時這里又發生了一次上下文切換。
被喚醒的等待線程在繼續運行時,需要再次申請相應對象的內部鎖,此時等待線程可能需要和其它新來的活躍線程爭用內部鎖,這也可能會導致上下文切換。
如果有多個消費者線程同時被阻塞,用 notifyAll() 方法,將會喚醒所有阻塞的線程。而某些商品依然沒有庫存,過早地喚醒這些沒有庫存的商品的消費線程,可能會導致線程再次進入阻塞狀態,從而引起不必要的上下文切換。
優化 wait/notify 的使用,減少上下文切換
首先,我們在多個不同消費場景中,可以使用 Object.notify() 替代 Object.notifyAll()。因為 Object.notify() 只會喚醒指定線程,不會過早地喚醒其它未滿足需求的阻塞線程,所以可以減少相應的上下文切換。
其次,在生產者執行完 Object.notify() / notifyAll() 喚醒其它線程之后,應該盡快地釋放內部鎖,以避免其它線程在喚醒之后長時間地持有鎖處理業務操作,這樣可以避免被喚醒的線程再次申請相應內部鎖的時候等待鎖的釋放。
最后,為了避免長時間等待,我們常會使用 Object.wait (long)設置等待超時時間,但線程無法區分其返回是由于等待超時還是被通知線程喚醒,從而導致線程再次嘗試獲取鎖操作,增加了上下文切換。
這里我建議使用 Lock 鎖結合 Condition 接口替代 Synchronized 內部鎖中的 wait / notify,實現等待/通知。這樣做不僅可以解決上述的 Object.wait(long) 無法區分的問題,還可以解決線程被過早喚醒的問題。
Condition 接口定義的 await 方法 、signal 方法和 signalAll 方法分別相當于 Object.wait()、 Object.notify() 和 Object.notifyAll()。
合理地設置線程池大小,避免創建過多線程
線程池的線程數量設置不宜過大,因為一旦線程池的工作線程總數超過系統所擁有的處理器數量,就會導致過多的上下文切換。更多關于如何合理設置線程池數量的內容,我將在后續詳解。
還有一種情況就是,在有些創建線程池的方法里,線程數量設置不會直接暴露給我們。比如,用 Executors.newCachedThreadPool() 創建的線程池,該線程池會復用其內部空閑的線程來處理新提交的任務,如果沒有,再創建新的線程(不受 MAX_VALUE 限制),這樣的線程池如果碰到大量且耗時長的任務場景,就會創建非常多的工作線程,從而導致頻繁的上下文切換。因此,這類線程池就只適合處理大量且耗時短的非阻塞任務。
使用協程實現非阻塞等待
相信很多人一聽到協程(Coroutines),馬上想到的就是 Go 語言。協程對于大部分 Java 程序員來說可能還有點陌生,但其在 Go 中的使用相對來說已經很成熟了。
協程是一種比線程更加輕量級的東西,相比于由操作系統內核來管理的進程和線程,協程則完全由程序本身所控制,也就是在用戶態執行。協程避免了像線程切換那樣產生的上下文切換,在性能方面得到了很大的提升。協程在多線程業務上的運用,我會在后續文章中詳述。
減少 Java 虛擬機的垃圾回收
我們在上一講講上下文切換的誘因時,曾提到過“垃圾回收會導致上下文切換”。
很多 JVM 垃圾回收器(serial 收集器、ParNew 收集器)在回收舊對象時,會產生內存碎片,從而需要進行內存整理,在這個過程中就需要移動存活的對象。而移動內存對象就意味著這些對象所在的內存地址會發生變化,因此在移動對象前需要暫停線程,在移動完成后需要再次喚醒該線程。因此減少 JVM 垃圾回收的頻率可以有效地減少上下文切換。
總結
上下文切換是多線程編程性能消耗的原因之一,而競爭鎖、線程間的通信以及過多地創建線程等多線程編程操作,都會給系統帶來上下文切換。除此之外,I/O 阻塞以及 JVM 的垃圾回收也會增加上下文切換。
總的來說,過于頻繁的上下文切換會影響系統的性能,所以我們應該避免它。另外,我們還可以將上下文切換也作為系統的性能參考指標,并將該指標納入到服務性能監控,防患于未然。
總結
以上是生活随笔為你收集整理的java 优化线程_Java | 多线程调优(下):如何优化多线程上下文切换?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: lstm网络_LSTM(长短期记忆网络)
- 下一篇: mysql实验步骤_MySQL双方配置实