現在的業務場景用到多線程的情況越來越多,那麼多線程調優就是一個無法避開的話題,而線程調優主要是避免鎖競爭,減少上下文切換。所以先簡單說說【鎖】。
在併發編程中,多個線程訪問同一個共享資源時,我們必須考慮如何維護數據的原子性,爲了保證數據的原子性,就必須用到鎖。
【鎖的分類】
Synchronized鎖 和 Lock鎖
【Synchronized同步鎖的實現原理】
Synchronized是JVM實現的一種內置鎖,鎖的獲取和釋放由JVM隱式實現。每次獲取和釋放鎖操作都會帶來用戶態和內核態的切換,從而增加了系統性能開銷。
在JDK1.6之前,Synchronized非常耗資源,JDK1.6開始,對Synchronized鎖進行了優化,性能有很大提高。
Synchronized同步鎖實現由兩種方式,一種是修飾方法,一種是修飾代碼塊。
// 關鍵字在實例方法上,鎖爲當前實例
public synchronized void method1() {
// code
}
// 關鍵字在代碼塊上,鎖爲括號裏面的對象
public void method2() {
Object o = new Object();
synchronized (o) {
// code
}
}
JVM中同步是基於進入和退出管程(Monitor)對象實現的,每個對象實例都會有一個Monitor,Monitor可以和對象一起創建、銷燬。多個線程同時訪問一段同步代碼時,多個線程會先被存放在EntryList集合中,處於block狀態的線程,都會被加入到該列表。接下來當線程獲取到Monitor對象時,Monitor是依靠底層操作系統的MutexLock來實現互斥,線程申請Mutex成功,則持有改Mutex,其他線程將無法獲取到該mutex;如果線程調用wait方法,就會釋放當前持有的mutex,並且該線程會進入到WaitSet集合中,等待下次被喚醒。如果當前線程順利執行完方法,就會釋放Mutex。
總的來說,Synchronized同步鎖的實現,因Monitor是依賴底層操作系統實現的,存在用戶態和內核態之間的切換,增加了系統性能開銷。爲了提高性能,JDK1.6引入了偏向鎖、輕量級鎖、重量級鎖的概念,來減少鎖競爭帶來的上下文切換,實現這個優化,是在java對象中新增了對象頭。在JDK1.6開始,對象實例在堆內存中被分爲三部分:對象頭、實例數據和對象充填。其中Java對象頭包括Mark Word、指向類的指針和數組長度組成。而Mark Word記錄了對象和鎖的有關信息。
Synchronized同步鎖就是從偏向鎖開始的,隨着競爭越來越激烈,偏向鎖升級到輕量級鎖,最終升級到重量級鎖。
JVM 在 JDK1.6 中引入了分級鎖機制來優化 Synchronized,當一個線程獲取鎖時,首先對象鎖將成爲一個偏向鎖,這樣做是爲了優化同一線程重複獲取導致的用戶態與內核態的切換問題;其次如果有多個線程競爭鎖資源,鎖將會升級爲輕量級鎖,它適用於在短時間內持有鎖,且分鎖有交替切換的場景;輕量級鎖還使用了自旋鎖來避免線程用戶態與內核態的頻繁切換,大大地提高了系統性能;但如果鎖競爭太激烈了,那麼同步鎖將會升級爲重量級鎖。
減少鎖競爭,是優化 Synchronized 同步鎖的關鍵。我們應該儘量使 Synchronized 同步鎖處於輕量級鎖或偏向鎖,這樣才能提高 Synchronized 同步鎖的性能;通過減小鎖粒度來降低鎖競爭也是一種最常用的優化方法;另外我們還可以通過減少鎖的持有時間來提高 Synchronized 同步鎖在自旋時獲取鎖資源的成功率,避免 Synchronized 同步鎖升級爲重量級鎖。
在高併發、高負載的場景下,我們可以通過設置JVM參數關閉偏向鎖和自旋鎖。
-XX:-UseBiasedLocking // 關閉偏向鎖(默認打開)
-XX:-UseSpinning // 參數關閉自旋鎖優化 (默認打開)
-XX:PreBlockSpin // 參數修改默認的自旋次數。JDK1.7 後,去掉此參數,由 jvm 控制
【Lock同步鎖】
Lock鎖是通過java代碼來實現顯示獲取和釋放的一種鎖。
從性能方面上來說,在併發量不高、競爭不激烈的情況下,Synchronized 同步鎖由於具有分級鎖的優勢,性能上與 Lock 鎖差不多;但在高負載、高併發的情況下,Synchronized 同步鎖由於競爭激烈會升級到重量級鎖,性能則沒有 Lock 鎖穩定。
【Lock同步鎖實現原理】
Lock是一個接口類,常用的實現類有ReenTrantLock(獨佔鎖)、ReentrantReadWriteLock(RRW讀寫鎖)
ReenTrantLock(獨佔鎖)
ReenTrantLock(獨佔鎖)同一時間只允許一個線程訪問。示例代碼如下:
public class MyService {
private Lock lock = new ReentrantLock();
public void testMethod() {
lock.lock();
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName()
+ (" " + (i + 1)));
}
lock.unlock();
}
}
我們知道,對於同一份數據進行讀寫,如果一個線程在讀數據,而另一個線程在寫數據,那麼讀到的數據和最終的數據就會不一致;如果一個線程在寫數據,而另一個線程也在寫數據,那麼線程前後看到的數據也會不一致。這個時候我們可以在讀寫方法中加入互斥鎖,來保證任何時候只能有一個線程進行讀或寫操作。
在大部分業務場景中,讀業務操作要遠遠大於寫業務操作。而在多線程編程中,讀操作並不會修改共享資源的數據,如果多個線程僅僅是讀取共享資源,那麼這種情況下其實沒有必要對資源進行加鎖。如果使用互斥鎖,反倒會影響業務的併發性能。那麼在這種場景下,有沒有什麼辦法可以優化下鎖的實現方式呢?
由於ReenTrantLock(獨佔鎖)同一時間只允許一個線程訪問,在一些業務場景中,比如讀多寫少的場景,這種場景是一種非常場景的場景,比如商品活動秒殺,總共有200件商品,但是可能好幾十萬人搶。幾十萬人讀數據,但是最多有200人是寫數據。
ReentrantReadWriteLock(RRW讀寫鎖)
針對這種讀多寫少的場景,Java 提供了另外一個實現 Lock 接口的讀寫鎖 RRW。我們已知 ReentrantLock 是一個獨佔鎖,同一時間只允許一個線程訪問,而 RRW 允許多個讀線程同時訪問,但不允許寫線程和讀線程、寫線程和寫線程同時訪問。讀寫鎖內部維護了兩個鎖,一個是用於讀操作的 ReadLock,一個是用於寫操作的 WriteLock。
public class TestRTTLock {
private double x, y;
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 讀鎖
private Lock readLock = lock.readLock();
// 寫鎖
private Lock writeLock = lock.writeLock();public double read() {
// 獲取讀鎖
readLock.lock();
try {
return Math.sqrt(x * x + y * y);
} finally {
// 釋放讀鎖
readLock.unlock();
}
}public void move(double deltaX, double deltaY) {
// 獲取寫鎖
writeLock.lock();
try {
x += deltaX;
y += deltaY;
} finally {
// 釋放寫鎖
writeLock.unlock();
}
}}
不管使用 Synchronized 同步鎖還是 Lock 同步鎖,只要存在鎖競爭就會產生線程阻塞,從而導致線程之間的頻繁切換,最終增加性能消耗。因此,如何降低鎖競爭,就成爲了優化鎖的關鍵。
在 Synchronized 同步鎖中,可以通過減小鎖粒度、減少鎖佔用時間來降低鎖的競爭。 Lock 鎖的靈活性,通過鎖分離的方式來降低鎖競爭。