在聊聊高并發(fā)(五)理解緩存一致性協(xié)議以及對并發(fā)編程的影響 我們了解了處理器緩存一致性協(xié)議的原理,并且提到了它對并發(fā)編程的影響残炮,“多個線程對同一個變量一直使用CAS操作,那么會有大量修改操作近她,從而產(chǎn)生大量的緩存一致性流量请唱,因?yàn)槊恳淮蜟AS操作都會發(fā)出廣播通知其他處理器跋选,從而影響程序的性能棍好〗锛牛”
這一篇我們通過兩種實(shí)現(xiàn)自旋鎖的方式來看一下不同的編程方式帶來的程序性能的變化识颊。
先理解一下什么是自旋崭庸,所謂自旋就是線程在不滿足某種條件的情況下,一直循環(huán)做某個動作谊囚。所以對于自旋鎖來鎖怕享,當(dāng)線程在沒有獲取鎖的情況下,一直循環(huán)嘗試獲取鎖镰踏,直到真正獲取鎖函筋。
在聊聊高并發(fā)(三)鎖的一些基本概念 我們提到鎖的本質(zhì)就是等待,那么如何等待呢奠伪,有兩種方式
1. 線程阻塞
2. 線程自旋
阻塞的缺點(diǎn)顯而易見跌帐,線程一旦進(jìn)入阻塞(Block),再被喚醒的代價比較高绊率,性能較差谨敛。自旋的優(yōu)點(diǎn)是線程還是Runnable的,只是在執(zhí)行空代碼滤否。當(dāng)然一直自旋也會白白消耗計算資源脸狸,所以常見的做法是先自旋一段時間,還沒拿到鎖就進(jìn)入阻塞。JVM在處理synchrized實(shí)現(xiàn)時就是采用了這種折中的方案炊甲,并提供了調(diào)節(jié)自旋的參數(shù)泥彤。
這篇說一下兩種最基本的自旋鎖實(shí)現(xiàn),并提供了一種優(yōu)化的鎖卿啡,后續(xù)會有更多的自旋鎖的實(shí)現(xiàn)吟吝。
首先是TASLock (Test And Set Lock),測試-設(shè)置鎖颈娜,它的特點(diǎn)是自旋時剑逃,每次嘗試獲取鎖時,采用了CAS操作官辽,不斷的設(shè)置鎖標(biāo)志位炕贵,當(dāng)鎖標(biāo)志位可用時,一個線程拿到鎖野崇,其他線程繼續(xù)自旋称开。
缺點(diǎn)是CAS操作一直在修改共享變量的值,會引發(fā)緩存一致性流量風(fēng)暴
package com.test.lock;
// 鎖接口
public interface Lock {
public void lock();
public void unlock();
}
package com.test.lock;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 測試-設(shè)置自旋鎖乓梨,使用AtomicBoolean原子變量保存狀態(tài)
* 每次都使用getAndSet原子操作來判斷鎖狀態(tài)并嘗試獲取鎖
* 缺點(diǎn)是getAndSet底層使用CAS來實(shí)現(xiàn)鳖轰,一直在修改共享變量的值,會引發(fā)緩存一致性流量風(fēng)暴
* **/
public class TASLock implements Lock{
private AtomicBoolean mutex = new AtomicBoolean(false);
@Override
public void lock() {
// getAndSet方法會設(shè)置mutex變量為true扶镀,并返回mutex之前的值
// 當(dāng)mutex之前是false時才返回蕴侣,表示獲取鎖
// getAndSet方法是原子操作,mutex原子變量的改動對所有線程可見
while(mutex.getAndSet(true)){
}
}
@Override
public void unlock() {
mutex.set(false);
}
public String toString(){
return "TASLock";
}
}
一種改進(jìn)的算法是TTASLock(Test Test And Set Lock)測試-測試-設(shè)置鎖臭觉,特點(diǎn)是在自旋嘗試獲取鎖時昆雀,分為兩步,第一步通過讀操作來獲取鎖狀態(tài)蝠筑,當(dāng)鎖可獲取時狞膘,第二步再通過CAS操作來嘗試獲取鎖,減少了CAS的操作次數(shù)什乙。并且第一步的讀操作是處理器直接讀取自身高速緩存挽封,不會產(chǎn)生緩存一致性流量,不占用總線資源臣镣。
缺點(diǎn)是在鎖高爭用的情況下辅愿,線程很難一次就獲取鎖,CAS的操作會大大增加忆某。
package com.test.lock;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 測試-測試-設(shè)置自旋鎖点待,使用AtomicBoolean原子變量保存狀態(tài)
* 分為兩步來獲取鎖
* 1. 先采用讀變量自旋的方式嘗試獲取鎖
* 2. 當(dāng)有可能獲取鎖時,再使用getAndSet原子操作來嘗試獲取鎖
* 優(yōu)點(diǎn)是第一步使用讀變量的方式來獲取鎖弃舒,在處理器內(nèi)部高速緩存操作癞埠,不會產(chǎn)生緩存一致性流量
* 缺點(diǎn)是當(dāng)鎖爭用激烈的時候,第一步一直獲取不到鎖,getAndSet底層使用CAS來實(shí)現(xiàn)燕差,一直在修改共享變量的值,會引發(fā)緩存一致性流量風(fēng)暴
* **/
public class TTASLock implements Lock{
private AtomicBoolean mutex = new AtomicBoolean(false);
@Override
public void lock() {
while(true){
// 第一步使用讀操作坝冕,嘗試獲取鎖徒探,當(dāng)mutex為false時退出循環(huán),表示可以獲取鎖
while(mutex.get()){}
// 第二部使用getAndSet方法來嘗試獲取鎖
if(!mutex.getAndSet(true)){
return;
}
}
}
@Override
public void unlock() {
mutex.set(false);
}
public String toString(){
return "TTASLock";
}
}
針對鎖高爭用的問題喂窟,可以采取回退算法测暗,即當(dāng)線程沒有拿到鎖時,就等待一段時間再去嘗試獲取鎖磨澡,這樣可以減少鎖的爭用碗啄,提高程序的性能。
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;
/**
* 回退自旋鎖,在測試-測試-設(shè)置自旋鎖的基礎(chǔ)上增加了線程回退厦酬,降低鎖的爭用
* 優(yōu)點(diǎn)是在鎖高爭用的情況下減少了鎖的爭用胆描,提高了執(zhí)行的性能
* 缺點(diǎn)是回退的時間難以控制,需要不斷測試才能找到合適的值仗阅,而且依賴底層硬件的性能昌讲,擴(kuò)展性差
* **/
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){
// 第一步使用讀操作,嘗試獲取鎖减噪,當(dāng)mutex為false時退出循環(huán)短绸,表示可以獲取鎖
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";
}
}
回退自旋鎖的問題是回退的時間難以控制,需要不斷測試才能找到合適的值筹裕,而且依賴底層硬件的性能醋闭,擴(kuò)展性差。后面會有更好的自旋鎖實(shí)現(xiàn)算法朝卒。
下面我們測試一下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();
}
}
}
測試機(jī)器的性能如下:
CPU: 4 Intel(R) Core(TM) i3-2120 CPU @ 3.30GHz
內(nèi)存: 8G
測試結(jié)果:
50個線程情況下:
TASLock平均獲取鎖的時間: 339715 ns
TTASLock平均獲取鎖的時間: 67106.2 ns
100個線程情況下:
TASLock平均獲取鎖的時間: 1198413 ns
TTASLock平均獲取鎖的時間: 1273588 ns
可以看到TTASLock的性能比TASLock的性能更好
轉(zhuǎn)載請注明來源: http://blog.csdn.net/iter_zc