為Java添加@atomic操作

jopen 10年前發布 | 8K 次閱讀 Java

Java中的原子操作是怎么工作的?在目前的OpenJDK/Hotspot中,是否有其他方式轉化為原子操作?

反饋

我在之前的文章 Making operations on volatile fields atomic中提到,如果沒有仔細斟酌,直接“修正”以前的功能是不合適的。

轉化原子操作,一種替代的方案是加入@atomic注解。這樣做的優點是可以在新代碼中使用,而且不會破會舊代碼的兼容性。

注:使用小寫的名字是有意的,因為它遵循當前的編碼習慣。

原子操作

任何域加上@atomic會使得整個表達式帶有原子性。非volatile或非atomic的變量,可以在表達式(加上@atomic的表達式)開始執行前讀取或者在表達式完成后設置。表達式本身可能需要上鎖,CAS(譯者注:compare and swap)操作或者TSX,取決于依賴于平臺的CPU。如果所有域是只讀的,或者只有一個域可寫,則與volatile的功能一致。

原子布爾型

現在的AtomicBoolean類加上對象頭以及可能的填充字節(與引用一樣)需要4字節。把這個域寫入代碼中,可能是這樣的:

@atomic boolean flag;
// toggle the flag.
this.flag = !this.flag;

這段代碼會如何運行呢?并不是所有的平臺都支持一字節的原子操作,比如Unsafe類就沒有一字節的CAS操作。這可以通過布爾屏蔽來實現。

// possible replacement.
while(true) {
    int num = Unsafe.getUnsafe().getVolatileInt(this, FLAG_OFFSET & ~3); // word align the access.
    int value ^= 1 << ~(0xFF << (FLAG_OFFSET & 3) * 8) ;
    if (Unsafe.getUnsafe().compareAndSwapInt(this, FLAG_OFFSET & ~3, num, value))
        break;
}

譯者注:上面一段代碼的while循環是一個CAS操作,確保取反操作的原子性。循環體第一句是獲取修改前的內容。flag變量占用一個字節,這里直接獲取包含flag變量的一個雙字(4字節)。第二句是計算flag取反后,這個雙字應該存放的內容,但這里的代碼應該有問題,讀者可以自己修改。第三句進行compareAndSwapInt操作,存入取反后的內容。整個取反過程保證了這四字節的內容不會被其他線程修改。)

原子雙精度

Java標準庫中不支持AtomicDouble,這里是基于AtomicLong的變種。請看下面的例子:

@atomic double a = 1;
volatile double b = 2;
a += b;

放在今天,可能會怎么實現呢?

while(true) {
    double _b = Unsafe.getUnsafe().getVolatileDouble(this, B_OFFSET);
    double _a = Unsafe.getUnsafe().getVolatileDouble(this, A_OFFSET);
    long aAsLong = Double.doubleToRawLongBits(_a);
    double _sum = _a + _b;
    long sumAsLong = Double.doubleToRawLongBits(_a);
    if (Unsafe.getUnsafe().compareAndSwapLong(this, A_OFFSET, aAsLong, sumAsLong))
        break;
}

兩個原子域

使用Intel的TSX,你可以把幾個域打包到一個硬件事務中。但如果你的平臺不支持TSX,又不使用鎖,這可行么?

@atomic int a = 1, b = 2;
a += b * (b % 2 == 0 ? 2 : 1);

如果有多個域一起是原子的,使用CAS仍然可行。將來會設計一個CAS2操作能夠檢查兩個64位的值。所以目前,下面的例子使用兩個4字節的變量。

assert A_OFFSET + 4 == B_OFFSET;
while(true) {
    long _ab = Unsafe.getUnsafe().getVolatileLong(this, A_OFFSET);
    int _a = getLowerInt(_ab);
    int _b = getHigherInt(_ab);
    int _sum = _a + _b * (_b % 2 == 0 ? 2 : 1);
    int _sum_ab = setLowerIntFor(_ab, _sum);
    if (Unsafe.getUnsafe().compareAndSwapLong(this, A_OFFSET, _ab, _sum_ab))
        break;
}

注意:這個操作能以原子的方式只修改a、只修改b或同時修改ab。

原子引用

一個作用在不可變對象上的普通用例,比如BigDecimal

@atomic BigDecimal a;
BigDecimal b;
a = a.add(b);

在啟用CompressedOops(普通對象指針壓縮)的系統或者是32位的JVM上,可以通過下面這種方式實現:

BigDecimal _b = this.b;
while(true) {
    BigDecimal _a = (BigDecimal) Unsafe.getUnsafe().getVolatileObject(this, A_OFFSET);
    BigDecimal _sum = _a.add(_b);
    if (Unsafe.getUnsafe().compareAndSwapLong(this, A_OFFSET, _a, _sum))
        break;
}

更復雜的例子

總會有這樣的例子,因為它們太復雜無法運行在你的系統中。它們可能能運行在支持TSX的系統上,或者支持HotSpot的系統上。但在你的系統上,可能需要一些回退(使用舊的技術達到目的)。

@atomic long a, b, c, d;
a = (b = (c = d + 4) + 5 ) + 6;

目前,上面的例子還不支持,它在一個表達式中設置了多個long值。但是,退一步說可以使用已有的鎖機制。

synchronized(this) {
    a = (b = (c = d + 4) + 5 ) + 6;
}

總結

通過添加注解,我們能為普通域增加原子操作而無需改變語法。這可以作為語言的自然擴展,而不會破壞后向兼容性。

原文鏈接: dzone 翻譯: ImportNew.com - 文 學敏
譯文鏈接: http://www.importnew.com/12438.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!