聊聊高併發(六)實現幾種自旋鎖(一)
在聊聊高併發(五)理解快取一致性協議以及對併發程式設計的影響 我們瞭解了處理器快取一致性協議的原理,並且提到了它對併發程式設計的影響,“多個執行緒對同一個變數一直使用CAS操作,那麼會有大量修改操作,從而產生大量的快取一致性流量,因為每一次CAS操作都會發出廣播通知其他處理器,從而影響程式的效能。”
這一篇我們通過兩種實現自旋鎖的方式來看一下不同的程式設計方式帶來的程式效能的變化。
先理解一下什麼是自旋,所謂自旋就是執行緒在不滿足某種條件的情況下,一直迴圈做某個動作。所以對於自旋鎖來鎖,當執行緒在沒有獲取鎖的情況下,一直迴圈嘗試獲取鎖,直到真正獲取鎖。
1. 執行緒阻塞
2. 執行緒自旋
阻塞的缺點顯而易見,執行緒一旦進入阻塞(Block),再被喚醒的代價比較高,效能較差。自旋的優點是執行緒還是Runnable的,只是在執行空程式碼。當然一直自旋也會白白消耗計算資源,所以常見的做法是先自旋一段時間,還沒拿到鎖就進入阻塞。JVM在處理synchrized實現時就是採用了這種折中的方案,並提供了調節自旋的引數。
這篇說一下兩種最基本的自旋鎖實現,並提供了一種優化的鎖,後續會有更多的自旋鎖的實現。
首先是TASLock (Test And Set Lock),測試-設定鎖,它的特點是自旋時,每次嘗試獲取鎖時,採用了CAS操作,不斷的設定鎖標誌位,當鎖標誌位可用時,一個執行緒拿到鎖,其他執行緒繼續自旋。
缺點是CAS操作一直在修改共享變數的值,會引發快取一致性流量風暴
package com.test.lock; // 鎖介面 public interface Lock { public void lock(); public void unlock(); } package com.test.lock; import java.util.concurrent.atomic.AtomicBoolean; /** * 測試-設定自旋鎖,使用AtomicBoolean原子變數儲存狀態 * 每次都使用getAndSet原子操作來判斷鎖狀態並嘗試獲取鎖 * 缺點是getAndSet底層使用CAS來實現,一直在修改共享變數的值,會引發快取一致性流量風暴 * **/ public class TASLock implements Lock{ private AtomicBoolean mutex = new AtomicBoolean(false); @Override public void lock() { // getAndSet方法會設定mutex變數為true,並返回mutex之前的值 // 當mutex之前是false時才返回,表示獲取鎖 // getAndSet方法是原子操作,mutex原子變數的改動對所有執行緒可見 while(mutex.getAndSet(true)){ } } @Override public void unlock() { mutex.set(false); } public String toString(){ return "TASLock"; } }
一種改進的演算法是TTASLock(Test Test And Set Lock)測試-測試-設定鎖,特點是在自旋嘗試獲取鎖時,分為兩步,第一步通過讀操作來獲取鎖狀態,當鎖可獲取時,第二步再通過CAS操作來嘗試獲取鎖,減少了CAS的操作次數。並且第一步的讀操作是處理器直接讀取自身快取記憶體,不會產生快取一致性流量,不佔用匯流排資源。
缺點是在鎖高爭用的情況下,執行緒很難一次就獲取鎖,CAS的操作會大大增加。
package com.test.lock;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 測試-測試-設定自旋鎖,使用AtomicBoolean原子變數儲存狀態
* 分為兩步來獲取鎖
* 1. 先採用讀變數自旋的方式嘗試獲取鎖
* 2. 當有可能獲取鎖時,再使用getAndSet原子操作來嘗試獲取鎖
* 優點是第一步使用讀變數的方式來獲取鎖,在處理器內部快取記憶體操作,不會產生快取一致性流量
* 缺點是當鎖爭用激烈的時候,第一步一直獲取不到鎖,getAndSet底層使用CAS來實現,一直在修改共享變數的值,會引發快取一致性流量風暴
* **/
public class TTASLock implements Lock{
private AtomicBoolean mutex = new AtomicBoolean(false);
@Override
public void lock() {
while(true){
// 第一步使用讀操作,嘗試獲取鎖,當mutex為false時退出迴圈,表示可以獲取鎖
while(mutex.get()){}
// 第二部使用getAndSet方法來嘗試獲取鎖
if(!mutex.getAndSet(true)){
return;
}
}
}
@Override
public void unlock() {
mutex.set(false);
}
public String toString(){
return "TTASLock";
}
}
針對鎖高爭用的問題,可以採取回退演算法,即當執行緒沒有拿到鎖時,就等待一段時間再去嘗試獲取鎖,這樣可以減少鎖的爭用,提高程式的效能。
package com.test.lock;
import java.util.Random;
/**
* 回退演算法,降低鎖爭用的機率
* **/
public class Backoff {
private final int minDelay, maxDelay;
private int limit;
final Random random;
public Backoff(int min, int max){
this.minDelay = min;
this.maxDelay = max;
limit = minDelay;
random = new Random();
}
// 回退,執行緒等待一段時間
public void backoff() throws InterruptedException{
int delay = random.nextInt(limit);
limit = Math.min(maxDelay, 2 * limit);
Thread.sleep(delay);
}
}
package com.test.lock;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 回退自旋鎖,在測試-測試-設定自旋鎖的基礎上增加了執行緒回退,降低鎖的爭用
* 優點是在鎖高爭用的情況下減少了鎖的爭用,提高了執行的效能
* 缺點是回退的時間難以控制,需要不斷測試才能找到合適的值,而且依賴底層硬體的效能,擴充套件性差
* **/
public class BackoffLock implements Lock{
private final int MIN_DELAY, MAX_DELAY;
public BackoffLock(int min, int max){
MIN_DELAY = min;
MAX_DELAY = max;
}
private AtomicBoolean mutex = new AtomicBoolean(false);
@Override
public void lock() {
// 增加回退物件
Backoff backoff = new Backoff(MIN_DELAY, MAX_DELAY);
while(true){
// 第一步使用讀操作,嘗試獲取鎖,當mutex為false時退出迴圈,表示可以獲取鎖
while(mutex.get()){}
// 第二部使用getAndSet方法來嘗試獲取鎖
if(!mutex.getAndSet(true)){
return;
}else{
//回退
try {
backoff.backoff();
} catch (InterruptedException e) {
}
}
}
}
@Override
public void unlock() {
mutex.set(false);
}
public String toString(){
return "TTASLock";
}
}
回退自旋鎖的問題是回退的時間難以控制,需要不斷測試才能找到合適的值,而且依賴底層硬體的效能,擴充套件性差。後面會有更好的自旋鎖實現演算法。
下面我們測試一下TASLock和TTASLock的效能。
首先寫一個計時的類
package com.test.lock;
public class TimeCost implements Lock{
private final Lock lock;
public TimeCost(Lock lock){
this.lock = lock;
}
@Override
public void lock() {
long start = System.nanoTime();
lock.lock();
long duration = System.nanoTime() - start;
System.out.println(lock.toString() + " time cost is " + duration + " ns");
}
@Override
public void unlock() {
lock.unlock();
}
}
然後採用多個執行緒來模擬對同一把鎖的爭用
package com.test.lock;
public class Main {
private static TimeCost timeCost = new TimeCost(new TASLock());
//private static TimeCost timeCost = new TimeCost(new TTASLock());
public static void method(){
timeCost.lock();
//int a = 10;
timeCost.unlock();
}
public static void main(String[] args) {
for(int i = 0; i < 100; i ++){
Thread t = new Thread(new Runnable(){
@Override
public void run() {
method();
}
});
t.start();
}
}
}
測試機器的效能如下:
CPU: 4 Intel(R) Core(TM) i3-2120 CPU @ 3.30GHz
記憶體: 8G
測試結果:
50個執行緒情況下:
TASLock平均獲取鎖的時間: 339715 ns
TTASLock平均獲取鎖的時間: 67106.2 ns
100個執行緒情況下:
TASLock平均獲取鎖的時間: 1198413 ns
TTASLock平均獲取鎖的時間: 1273588 ns
可以看到TTASLock的效能比TASLock的效能更好