'探索JAVA併發,如何減少鎖的競爭'

Java 睡眠 程序員梅長蘇 2019-08-16
"
"
探索JAVA併發,如何減少鎖的競爭

鎖的競爭會限制代碼的可伸縮性,在併發編程時通過一些手段有意地減少鎖競爭,可以讓程序有更好的表現。

所謂可伸縮性,即當增加計算資源(如CPU、內存、帶寬等)時,程序的吞吐量或處理能力會相應增加。這個時候,我們當然希望增加的效果越明顯越好,不過如果鎖競爭太嚴重,可伸縮性會大打折扣。

縮小鎖的範圍

當某個方法需要操作一個線程不安全的共享資源時,最簡單的辦法就是給方法加上synchronized,這樣一來這個方法只能同時有一個線程在執行,滿滿的安全感。


public class Counter {
private volatile int value;
public synchronized void incr(int n) {
System.out.println("i will incr " + n);
try {
// 這個小小的睡眠代表一些線程安全的操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i am ready");
value = value + n;
System.out.println("i incr " + n);
}
//...
}

上述示例的同步方法中有個耗時1秒的準備過程,這個過程是線程安全,但由於身在同步方法中,眾線程不得不排隊睡覺。這時候不管增加多少個線程,程序該睡多久還是睡多久。若是把這個步驟從同步代碼塊中移除,大家就能併發睡覺。


public class Counter {
private volatile int value;
public void incr(int n) {
System.out.println("i will incr " + n);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i am ready");
synchronized (this) {
// 只有這行代碼是不能併發執行的
value = value + n;
}
System.out.println("i incr " + n);
}
//...
}

通過上述示例就讓線程在持有鎖時需要執行的指令儘可能小,併發的效率更高了。

但如果多個線程不安全的操作之間隔著一些安全的耗時操作,是分別使用同步塊,還是用一個同步塊,並不能說誰一定好。因為同步代碼塊也是有額外性能開銷的,比起同步執行無關的操作,不一定划算,還是需要通過測試,用數據說話。

減小鎖粒度 - 鎖分解

如果一個鎖要用來保護多個相互獨立的資源,可以考慮用多個鎖分別保護某個資源,即鎖分解。

如此這般,就不用在只需要操作一個資源時,把其它不相干資源也捲入其中,導致其它想用資源的線程看著這個線程佔著茅坑不拉(或者佔著整個廁所更恰當?)。

使用一個鎖保護多個資源

下面這個示例,不管想操作哪個資源,都會把所有資源都鎖住。


public class Counter {
private volatile int src1;
private volatile int src2;
private volatile int src3;
public synchronized void incr(int src1, int src2, int src3) {
this.src1 += src1;
this.src2 += src2;
this.src3 += src3;
}
}

一個鎖保護一個資源

通過鎖分解,每個資源有它自己的鎖,可以單獨操作。如果想兼容舊代碼也是可以的。


public class Counter {
private volatile int src1;
private volatile int src2;
private volatile int src3;
// 兼容舊代碼,不用修改調用的地方
public void incr(int src1, int src2, int src3) {
if (src1 != 0) {
incrSrc1(src1);
}
if (src2 != 0) {
incrSrc2(src2);
}
if (src3 != 0) {
incrSrc3(src3);
}
}
public synchronized void incrSrc1(int n) {
this.src1 += n;
}
public synchronized void incrSrc2(int n) {
this.src2 += n;
}
public synchronized void incrSrc3(int n) {
this.src3 += n;
}
}

減小鎖粒度 - 鎖分段

鎖分段是鎖分解的進一步擴展,對於一組資源集合,可以把資源分為多個小組,每個小組用一個鎖來保護,比如我們熟知的ConcurrentHashMap(java8中已經不再使用分段鎖了,改為synchronized + cas)。

用的java8,不能分析一波ConcurrentHashMap的分段鎖了,寫個例子。


public class Counter {
private int[] src;
private Object[] locks;
/**
* @param nSrc 資源數量
* @param nLocks 分成幾段鎖
*/
public Counter(int nSrc, int nLocks) {
src = new int[nSrc];
locks = new Object[nLocks];
for (int i = 0; i < nLocks; i++) {
locks[i] = new Object();
}
}
/**
* @param idx 要訪問的資源序號
* @param n 增量
*/
public void incr(int idx, int n) {
// 根據一定規則(比如hash)找到目標資源歸誰管
synchronized (locks[idx % locks.length]) {
src[idx] += n;
}
}
//...
}

避免熱點域

上面的例子通過鎖分段減小了鎖的競爭,因為訪問不同段的資源時,需要的鎖是不同的,競爭壓力也隨之減小。畢竟比起10個人競爭一個名額,10個人競爭5個名額的話大家衝突不會那麼大。

但是,依然會存在需要同時訪問多個資源的情況,比如計算當前所有資源的總和,這個時候鎖的粒度就很難降低了。當鎖的粒度無法降低時,為了減少等待的時間,機智的程序員往往會用一些優化措施,比如把計算的結果緩存起來,熱點域就隨之被引入了。

依然以上面的代碼為例,增加一個計數器來記錄資源的變化,每個資源變化都修改計數器,這樣當需要統計所有資源時,只需要返回計數器的值就行了。這個計數器就是一個熱點域。

全局計數器引入熱點域


public class Counter {
private int[] src;
private Object[] locks;
// 全局計數器
private volatile int count;
/**
* @param nSrc 資源數量
* @param nLocks 分成幾段鎖
*/
public Counter(int nSrc, int nLocks) {
src = new int[nSrc];
locks = new Object[nLocks];
for (int i = 0; i < nLocks; i++) {
locks[i] = new Object();
}
}
/**
* @param idx 要訪問的資源序號
* @param n 增量
*/
public void incr(int idx, int n) {
// 根據一定規則(比如hash)找到目標資源歸誰管
synchronized (locks[idx % locks.length]) {
src[idx] += n;
}
// 不管操作哪個分段的資源,計數時都競爭同一個鎖
synchronized (this) {
count++;
}
}
// 直接返回緩存的值
public int count() {
return count;
}

//...
}

分段計數器避免熱點域

上述通過全局計數器緩存計算的結果雖然讓獲取計數方法的開銷從O(n)變成了O(1),但卻引入了熱點域,每次訪問資源都要訪問同一個計數器,這時候對可伸縮性就產生了一定影響,因為不管怎麼增加併發資源,在訪問計數器時都會有競爭。

ConcurrentHashMap中的做法是為每段數據單獨維護一個計數器,然後獲取總數時再對所有分段的計數做一個累加(真實情況會更復雜,比如ConcurrentHashMap會計算兩次modCount並比較,如果不相等表示計算過程有變動,就會給所有分段加鎖再累加)。

對全局計數器的例子做了簡單的改寫,去掉了熱點域。但換個角度,這樣卻也讓獲取總數的方法性能受到了影響,因此實際操作時還需要根據業務場景權衡利弊。魚和熊掌不可兼得,雖然很想說我全都要。


public class Counter {
private int[] src;
private Lock[] locks;
/**
* @param nSrc 資源數量
* @param nLocks 分成幾段鎖
*/
public Counter(int nSrc, int nLocks) {
src = new int[nSrc];
locks = new Lock[nLocks];
for (int i = 0; i < nLocks; i++) {
locks[i] = new Lock();
}
}
/**
* @param idx 要訪問的資源序號
* @param n 增量
*/
public void incr(int idx, int n) {
// 根據一定規則(比如hash)找到目標資源歸誰管
int lockIdx = idx % locks.length;
synchronized (locks[lockIdx]) {
src[idx] += n;
locks[lockIdx].count++;
}
}
public int count() {
// 就不像ConcurrentHashMap那麼嚴謹了,意思一下
int sum = 0;
for (Lock lock : locks) {
sum += lock.count;
}
return sum;
}
// 鎖
private static class Lock {
volatile int count;
}
//...
}

替代獨佔鎖

有時候可以選擇放棄使用獨佔鎖,改用更加友好的併發方式。

讀寫鎖

讀寫鎖(ReentrantReadWriteLock)維護了一對鎖(一個讀鎖和一個寫鎖),通過分離讀鎖和寫鎖,使得併發性相比一般的排他鎖有了很大提升。

在讀比寫多的場景下,使用讀寫鎖往往比一般的獨佔鎖有更好的性能表現。

原子變量

原子變量可以降低熱點域的更新開銷,但無法消除。

java.util.concurrent.atomic.* 包下有一些對應基本類型的原子變量類,使用了操作系統底層的能力,使用CAS(比較並交換,compare-and-swap)更新值。

檢測CPU利用率

通過檢測CPU的利用率,分析出可能限制程序性能的點,做出相應措施。

CPU利用率不均勻

多核的機器上,如果某個CPU忙成,其它CPU就在旁邊喊666,那證明當前程序的的大部分計算工作都由一小組線程在做。這時候可以考慮把這部分工作多拆分幾個線程來做(比如參考CPU數)。


"
探索JAVA併發,如何減少鎖的競爭

鎖的競爭會限制代碼的可伸縮性,在併發編程時通過一些手段有意地減少鎖競爭,可以讓程序有更好的表現。

所謂可伸縮性,即當增加計算資源(如CPU、內存、帶寬等)時,程序的吞吐量或處理能力會相應增加。這個時候,我們當然希望增加的效果越明顯越好,不過如果鎖競爭太嚴重,可伸縮性會大打折扣。

縮小鎖的範圍

當某個方法需要操作一個線程不安全的共享資源時,最簡單的辦法就是給方法加上synchronized,這樣一來這個方法只能同時有一個線程在執行,滿滿的安全感。


public class Counter {
private volatile int value;
public synchronized void incr(int n) {
System.out.println("i will incr " + n);
try {
// 這個小小的睡眠代表一些線程安全的操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i am ready");
value = value + n;
System.out.println("i incr " + n);
}
//...
}

上述示例的同步方法中有個耗時1秒的準備過程,這個過程是線程安全,但由於身在同步方法中,眾線程不得不排隊睡覺。這時候不管增加多少個線程,程序該睡多久還是睡多久。若是把這個步驟從同步代碼塊中移除,大家就能併發睡覺。


public class Counter {
private volatile int value;
public void incr(int n) {
System.out.println("i will incr " + n);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i am ready");
synchronized (this) {
// 只有這行代碼是不能併發執行的
value = value + n;
}
System.out.println("i incr " + n);
}
//...
}

通過上述示例就讓線程在持有鎖時需要執行的指令儘可能小,併發的效率更高了。

但如果多個線程不安全的操作之間隔著一些安全的耗時操作,是分別使用同步塊,還是用一個同步塊,並不能說誰一定好。因為同步代碼塊也是有額外性能開銷的,比起同步執行無關的操作,不一定划算,還是需要通過測試,用數據說話。

減小鎖粒度 - 鎖分解

如果一個鎖要用來保護多個相互獨立的資源,可以考慮用多個鎖分別保護某個資源,即鎖分解。

如此這般,就不用在只需要操作一個資源時,把其它不相干資源也捲入其中,導致其它想用資源的線程看著這個線程佔著茅坑不拉(或者佔著整個廁所更恰當?)。

使用一個鎖保護多個資源

下面這個示例,不管想操作哪個資源,都會把所有資源都鎖住。


public class Counter {
private volatile int src1;
private volatile int src2;
private volatile int src3;
public synchronized void incr(int src1, int src2, int src3) {
this.src1 += src1;
this.src2 += src2;
this.src3 += src3;
}
}

一個鎖保護一個資源

通過鎖分解,每個資源有它自己的鎖,可以單獨操作。如果想兼容舊代碼也是可以的。


public class Counter {
private volatile int src1;
private volatile int src2;
private volatile int src3;
// 兼容舊代碼,不用修改調用的地方
public void incr(int src1, int src2, int src3) {
if (src1 != 0) {
incrSrc1(src1);
}
if (src2 != 0) {
incrSrc2(src2);
}
if (src3 != 0) {
incrSrc3(src3);
}
}
public synchronized void incrSrc1(int n) {
this.src1 += n;
}
public synchronized void incrSrc2(int n) {
this.src2 += n;
}
public synchronized void incrSrc3(int n) {
this.src3 += n;
}
}

減小鎖粒度 - 鎖分段

鎖分段是鎖分解的進一步擴展,對於一組資源集合,可以把資源分為多個小組,每個小組用一個鎖來保護,比如我們熟知的ConcurrentHashMap(java8中已經不再使用分段鎖了,改為synchronized + cas)。

用的java8,不能分析一波ConcurrentHashMap的分段鎖了,寫個例子。


public class Counter {
private int[] src;
private Object[] locks;
/**
* @param nSrc 資源數量
* @param nLocks 分成幾段鎖
*/
public Counter(int nSrc, int nLocks) {
src = new int[nSrc];
locks = new Object[nLocks];
for (int i = 0; i < nLocks; i++) {
locks[i] = new Object();
}
}
/**
* @param idx 要訪問的資源序號
* @param n 增量
*/
public void incr(int idx, int n) {
// 根據一定規則(比如hash)找到目標資源歸誰管
synchronized (locks[idx % locks.length]) {
src[idx] += n;
}
}
//...
}

避免熱點域

上面的例子通過鎖分段減小了鎖的競爭,因為訪問不同段的資源時,需要的鎖是不同的,競爭壓力也隨之減小。畢竟比起10個人競爭一個名額,10個人競爭5個名額的話大家衝突不會那麼大。

但是,依然會存在需要同時訪問多個資源的情況,比如計算當前所有資源的總和,這個時候鎖的粒度就很難降低了。當鎖的粒度無法降低時,為了減少等待的時間,機智的程序員往往會用一些優化措施,比如把計算的結果緩存起來,熱點域就隨之被引入了。

依然以上面的代碼為例,增加一個計數器來記錄資源的變化,每個資源變化都修改計數器,這樣當需要統計所有資源時,只需要返回計數器的值就行了。這個計數器就是一個熱點域。

全局計數器引入熱點域


public class Counter {
private int[] src;
private Object[] locks;
// 全局計數器
private volatile int count;
/**
* @param nSrc 資源數量
* @param nLocks 分成幾段鎖
*/
public Counter(int nSrc, int nLocks) {
src = new int[nSrc];
locks = new Object[nLocks];
for (int i = 0; i < nLocks; i++) {
locks[i] = new Object();
}
}
/**
* @param idx 要訪問的資源序號
* @param n 增量
*/
public void incr(int idx, int n) {
// 根據一定規則(比如hash)找到目標資源歸誰管
synchronized (locks[idx % locks.length]) {
src[idx] += n;
}
// 不管操作哪個分段的資源,計數時都競爭同一個鎖
synchronized (this) {
count++;
}
}
// 直接返回緩存的值
public int count() {
return count;
}

//...
}

分段計數器避免熱點域

上述通過全局計數器緩存計算的結果雖然讓獲取計數方法的開銷從O(n)變成了O(1),但卻引入了熱點域,每次訪問資源都要訪問同一個計數器,這時候對可伸縮性就產生了一定影響,因為不管怎麼增加併發資源,在訪問計數器時都會有競爭。

ConcurrentHashMap中的做法是為每段數據單獨維護一個計數器,然後獲取總數時再對所有分段的計數做一個累加(真實情況會更復雜,比如ConcurrentHashMap會計算兩次modCount並比較,如果不相等表示計算過程有變動,就會給所有分段加鎖再累加)。

對全局計數器的例子做了簡單的改寫,去掉了熱點域。但換個角度,這樣卻也讓獲取總數的方法性能受到了影響,因此實際操作時還需要根據業務場景權衡利弊。魚和熊掌不可兼得,雖然很想說我全都要。


public class Counter {
private int[] src;
private Lock[] locks;
/**
* @param nSrc 資源數量
* @param nLocks 分成幾段鎖
*/
public Counter(int nSrc, int nLocks) {
src = new int[nSrc];
locks = new Lock[nLocks];
for (int i = 0; i < nLocks; i++) {
locks[i] = new Lock();
}
}
/**
* @param idx 要訪問的資源序號
* @param n 增量
*/
public void incr(int idx, int n) {
// 根據一定規則(比如hash)找到目標資源歸誰管
int lockIdx = idx % locks.length;
synchronized (locks[lockIdx]) {
src[idx] += n;
locks[lockIdx].count++;
}
}
public int count() {
// 就不像ConcurrentHashMap那麼嚴謹了,意思一下
int sum = 0;
for (Lock lock : locks) {
sum += lock.count;
}
return sum;
}
// 鎖
private static class Lock {
volatile int count;
}
//...
}

替代獨佔鎖

有時候可以選擇放棄使用獨佔鎖,改用更加友好的併發方式。

讀寫鎖

讀寫鎖(ReentrantReadWriteLock)維護了一對鎖(一個讀鎖和一個寫鎖),通過分離讀鎖和寫鎖,使得併發性相比一般的排他鎖有了很大提升。

在讀比寫多的場景下,使用讀寫鎖往往比一般的獨佔鎖有更好的性能表現。

原子變量

原子變量可以降低熱點域的更新開銷,但無法消除。

java.util.concurrent.atomic.* 包下有一些對應基本類型的原子變量類,使用了操作系統底層的能力,使用CAS(比較並交換,compare-and-swap)更新值。

檢測CPU利用率

通過檢測CPU的利用率,分析出可能限制程序性能的點,做出相應措施。

CPU利用率不均勻

多核的機器上,如果某個CPU忙成,其它CPU就在旁邊喊666,那證明當前程序的的大部分計算工作都由一小組線程在做。這時候可以考慮把這部分工作多拆分幾個線程來做(比如參考CPU數)。


探索JAVA併發,如何減少鎖的競爭


CPU利用不充分

和CPU利用率不均勻的區別在於,他可能是均勻的,就是大家都在磨洋工。


"
探索JAVA併發,如何減少鎖的競爭

鎖的競爭會限制代碼的可伸縮性,在併發編程時通過一些手段有意地減少鎖競爭,可以讓程序有更好的表現。

所謂可伸縮性,即當增加計算資源(如CPU、內存、帶寬等)時,程序的吞吐量或處理能力會相應增加。這個時候,我們當然希望增加的效果越明顯越好,不過如果鎖競爭太嚴重,可伸縮性會大打折扣。

縮小鎖的範圍

當某個方法需要操作一個線程不安全的共享資源時,最簡單的辦法就是給方法加上synchronized,這樣一來這個方法只能同時有一個線程在執行,滿滿的安全感。


public class Counter {
private volatile int value;
public synchronized void incr(int n) {
System.out.println("i will incr " + n);
try {
// 這個小小的睡眠代表一些線程安全的操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i am ready");
value = value + n;
System.out.println("i incr " + n);
}
//...
}

上述示例的同步方法中有個耗時1秒的準備過程,這個過程是線程安全,但由於身在同步方法中,眾線程不得不排隊睡覺。這時候不管增加多少個線程,程序該睡多久還是睡多久。若是把這個步驟從同步代碼塊中移除,大家就能併發睡覺。


public class Counter {
private volatile int value;
public void incr(int n) {
System.out.println("i will incr " + n);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i am ready");
synchronized (this) {
// 只有這行代碼是不能併發執行的
value = value + n;
}
System.out.println("i incr " + n);
}
//...
}

通過上述示例就讓線程在持有鎖時需要執行的指令儘可能小,併發的效率更高了。

但如果多個線程不安全的操作之間隔著一些安全的耗時操作,是分別使用同步塊,還是用一個同步塊,並不能說誰一定好。因為同步代碼塊也是有額外性能開銷的,比起同步執行無關的操作,不一定划算,還是需要通過測試,用數據說話。

減小鎖粒度 - 鎖分解

如果一個鎖要用來保護多個相互獨立的資源,可以考慮用多個鎖分別保護某個資源,即鎖分解。

如此這般,就不用在只需要操作一個資源時,把其它不相干資源也捲入其中,導致其它想用資源的線程看著這個線程佔著茅坑不拉(或者佔著整個廁所更恰當?)。

使用一個鎖保護多個資源

下面這個示例,不管想操作哪個資源,都會把所有資源都鎖住。


public class Counter {
private volatile int src1;
private volatile int src2;
private volatile int src3;
public synchronized void incr(int src1, int src2, int src3) {
this.src1 += src1;
this.src2 += src2;
this.src3 += src3;
}
}

一個鎖保護一個資源

通過鎖分解,每個資源有它自己的鎖,可以單獨操作。如果想兼容舊代碼也是可以的。


public class Counter {
private volatile int src1;
private volatile int src2;
private volatile int src3;
// 兼容舊代碼,不用修改調用的地方
public void incr(int src1, int src2, int src3) {
if (src1 != 0) {
incrSrc1(src1);
}
if (src2 != 0) {
incrSrc2(src2);
}
if (src3 != 0) {
incrSrc3(src3);
}
}
public synchronized void incrSrc1(int n) {
this.src1 += n;
}
public synchronized void incrSrc2(int n) {
this.src2 += n;
}
public synchronized void incrSrc3(int n) {
this.src3 += n;
}
}

減小鎖粒度 - 鎖分段

鎖分段是鎖分解的進一步擴展,對於一組資源集合,可以把資源分為多個小組,每個小組用一個鎖來保護,比如我們熟知的ConcurrentHashMap(java8中已經不再使用分段鎖了,改為synchronized + cas)。

用的java8,不能分析一波ConcurrentHashMap的分段鎖了,寫個例子。


public class Counter {
private int[] src;
private Object[] locks;
/**
* @param nSrc 資源數量
* @param nLocks 分成幾段鎖
*/
public Counter(int nSrc, int nLocks) {
src = new int[nSrc];
locks = new Object[nLocks];
for (int i = 0; i < nLocks; i++) {
locks[i] = new Object();
}
}
/**
* @param idx 要訪問的資源序號
* @param n 增量
*/
public void incr(int idx, int n) {
// 根據一定規則(比如hash)找到目標資源歸誰管
synchronized (locks[idx % locks.length]) {
src[idx] += n;
}
}
//...
}

避免熱點域

上面的例子通過鎖分段減小了鎖的競爭,因為訪問不同段的資源時,需要的鎖是不同的,競爭壓力也隨之減小。畢竟比起10個人競爭一個名額,10個人競爭5個名額的話大家衝突不會那麼大。

但是,依然會存在需要同時訪問多個資源的情況,比如計算當前所有資源的總和,這個時候鎖的粒度就很難降低了。當鎖的粒度無法降低時,為了減少等待的時間,機智的程序員往往會用一些優化措施,比如把計算的結果緩存起來,熱點域就隨之被引入了。

依然以上面的代碼為例,增加一個計數器來記錄資源的變化,每個資源變化都修改計數器,這樣當需要統計所有資源時,只需要返回計數器的值就行了。這個計數器就是一個熱點域。

全局計數器引入熱點域


public class Counter {
private int[] src;
private Object[] locks;
// 全局計數器
private volatile int count;
/**
* @param nSrc 資源數量
* @param nLocks 分成幾段鎖
*/
public Counter(int nSrc, int nLocks) {
src = new int[nSrc];
locks = new Object[nLocks];
for (int i = 0; i < nLocks; i++) {
locks[i] = new Object();
}
}
/**
* @param idx 要訪問的資源序號
* @param n 增量
*/
public void incr(int idx, int n) {
// 根據一定規則(比如hash)找到目標資源歸誰管
synchronized (locks[idx % locks.length]) {
src[idx] += n;
}
// 不管操作哪個分段的資源,計數時都競爭同一個鎖
synchronized (this) {
count++;
}
}
// 直接返回緩存的值
public int count() {
return count;
}

//...
}

分段計數器避免熱點域

上述通過全局計數器緩存計算的結果雖然讓獲取計數方法的開銷從O(n)變成了O(1),但卻引入了熱點域,每次訪問資源都要訪問同一個計數器,這時候對可伸縮性就產生了一定影響,因為不管怎麼增加併發資源,在訪問計數器時都會有競爭。

ConcurrentHashMap中的做法是為每段數據單獨維護一個計數器,然後獲取總數時再對所有分段的計數做一個累加(真實情況會更復雜,比如ConcurrentHashMap會計算兩次modCount並比較,如果不相等表示計算過程有變動,就會給所有分段加鎖再累加)。

對全局計數器的例子做了簡單的改寫,去掉了熱點域。但換個角度,這樣卻也讓獲取總數的方法性能受到了影響,因此實際操作時還需要根據業務場景權衡利弊。魚和熊掌不可兼得,雖然很想說我全都要。


public class Counter {
private int[] src;
private Lock[] locks;
/**
* @param nSrc 資源數量
* @param nLocks 分成幾段鎖
*/
public Counter(int nSrc, int nLocks) {
src = new int[nSrc];
locks = new Lock[nLocks];
for (int i = 0; i < nLocks; i++) {
locks[i] = new Lock();
}
}
/**
* @param idx 要訪問的資源序號
* @param n 增量
*/
public void incr(int idx, int n) {
// 根據一定規則(比如hash)找到目標資源歸誰管
int lockIdx = idx % locks.length;
synchronized (locks[lockIdx]) {
src[idx] += n;
locks[lockIdx].count++;
}
}
public int count() {
// 就不像ConcurrentHashMap那麼嚴謹了,意思一下
int sum = 0;
for (Lock lock : locks) {
sum += lock.count;
}
return sum;
}
// 鎖
private static class Lock {
volatile int count;
}
//...
}

替代獨佔鎖

有時候可以選擇放棄使用獨佔鎖,改用更加友好的併發方式。

讀寫鎖

讀寫鎖(ReentrantReadWriteLock)維護了一對鎖(一個讀鎖和一個寫鎖),通過分離讀鎖和寫鎖,使得併發性相比一般的排他鎖有了很大提升。

在讀比寫多的場景下,使用讀寫鎖往往比一般的獨佔鎖有更好的性能表現。

原子變量

原子變量可以降低熱點域的更新開銷,但無法消除。

java.util.concurrent.atomic.* 包下有一些對應基本類型的原子變量類,使用了操作系統底層的能力,使用CAS(比較並交換,compare-and-swap)更新值。

檢測CPU利用率

通過檢測CPU的利用率,分析出可能限制程序性能的點,做出相應措施。

CPU利用率不均勻

多核的機器上,如果某個CPU忙成,其它CPU就在旁邊喊666,那證明當前程序的的大部分計算工作都由一小組線程在做。這時候可以考慮把這部分工作多拆分幾個線程來做(比如參考CPU數)。


探索JAVA併發,如何減少鎖的競爭


CPU利用不充分

和CPU利用率不均勻的區別在於,他可能是均勻的,就是大家都在磨洋工。


探索JAVA併發,如何減少鎖的競爭


CPU利用不充分一般有以下幾個原因:

  • 負載不均衡:僧多粥少,一人能分點事做就不錯了。這種情況可以考慮增加工作量,不要憐惜它們;
  • I/O密集:程序就不是CPU密集型的,這種情況可以想辦法增加I/O效率(比如增加任務/併發、提高帶寬),以此來使CPU利用率得到一定提高;
  • 外部依賴限制:比如調用其它服務等待太久,瓶頸在別人那。可以像I/O密集那樣自我提升,實力足夠的話也可以改變別人;
  • 鎖競爭:本文探索的主題,可以在線程轉儲信息中尋找等待鎖的地方,因地制宜。

CPU忙碌

閒也不行,忙也不行,你還要我怎樣?要怎樣!

如果CPU們已經很忙了,證明工作還是很飽和的,如果還想提高效率,可以考慮加派CPU了。不過並不是增加了CPU效率就一定會提升,增加CPU後可能又會變成上面兩種情況,這是一個循環,當循環停止(無法通過上面的方式得到有效優化),我們的應用基本上達到一個所謂“極限”了。

不使用對象池

線程池的應用範圍很廣,比如各種連接池。當應用創建比較耗時、耗資源時也常用對象池技術。但有時候,高併發下操作對象池帶來的性能損耗(線程同步、鎖競爭、阻塞…)可能比起在需要的時候直接new一直對象更大。

通常,對象分配操作的開銷比線程同步的開銷更低。

總結

總的來說有3種方式可以降低鎖的競爭程度,上面的操作基本都是圍繞這3種方式來做的:

  1. 減少鎖的持有時間(如:縮小鎖範圍)
  2. 降低鎖的請求頻率(如:鎖分解,鎖分段)
  3. 使用帶有協調機制的獨佔鎖(如:分段鎖,讀寫鎖)

喜歡小編的就點個關注吧,每天帶你領略技術的魅力,趁年輕趕緊學習吧,私信回覆“學習”更有免費視頻資料可以領取。


"

相關推薦

推薦中...