簡體   English   中英

用Java編寫線程安全模塊計數器

[英]Writing a thread safe modular counter in Java

完全免責聲明:這不是一個真正的功課,但我標記它是因為它主要是一個自學習練習而不是實際的“工作”。

假設我想用Java編寫一個簡單的線程安全模塊計數器。 也就是說,如果模M為3,那么計數器應該無限循環0,1,2,0,1,2 0, 1, 2, 0, 1, 2, … ad。

這是一次嘗試:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicModularCounter {
    private final AtomicInteger tick = new AtomicInteger();
    private final int M;

    public AtomicModularCounter(int M) {
        this.M = M;
    }
    public int next() {
        return modulo(tick.getAndIncrement(), M);
    }
    private final static int modulo(int v, int M) {
        return ((v % M) + M) % M;
    }
}

我對此代碼的分析(可能有問題)是因為它使用了AtomicInteger ,即使沒有任何顯式的synchronized方法/塊,它也非常安全。

不幸的是,“算法”本身並不完全“有效”,因為當tick包圍Integer.MAX_VALUEnext()可能會返回錯誤的值,具體取決於模M 那是:

System.out.println(Integer.MAX_VALUE + 1 == Integer.MIN_VALUE); // true
System.out.println(modulo(Integer.MAX_VALUE, 3)); // 1
System.out.println(modulo(Integer.MIN_VALUE, 3)); // 1

也就是說,當模數為3並且tick包圍時1, 1next()兩次調用將返回1, 1

next()獲取無序值也可能存在問題,例如:

  1. Thread1調用next()
  2. Thread2調用next()
  3. Thread2完成tick.getAndIncrement() ,返回x
  4. 線程1完成tick.getAndIncrement()返回Y = X + 1(mod M)表示

在這里,除了前面提到的包裝問題, xy確實是這兩個next()調用返回的兩個正確值,但是根據指定計數器行為的方式,可以認為它們是亂序的。 也就是說,我們現在有(Thread1,y)(Thread2,x) ,但是可能真的應該指定(Thread1, x)(Thread2,y)是“正確的”行為。

因此,通過對單詞的一些定義, AtomicModularCounter線程安全的 ,但實際上不是原子的

所以問題是:

  • 我的分析是否正確? 如果沒有,請指出任何錯誤。
  • 我上面的陳述是使用正確的術語嗎? 如果沒有,那么正確的陳述是什么?
  • 如果上面提到的問題是真的,那你將如何解決?
  • 通過利用AtomicInteger的原子性,你能不使用synchronized修復它嗎?
  • 你會如何編寫它,使得tick本身由模數控制范圍,甚至沒有機會包裹Integer.MAX_VALUE
    • 如果需要,我們可以假設M至少是一個小於Integer.MAX_VALUE的訂單

附錄

這是List無序“問題”的類比。

  • Thread1調用add(first)
  • Thread2調用add(second)

現在,如果我們已經成功更新了列表,並添加了兩個元素,但是secondfirst之前,即最后,是“線程安全”嗎?

如果那是“線程安全的”,那么它不是什么? 也就是說,如果我們在上面的場景中指定first應該總是在second場景之前,那么該並發屬性被稱為什么? (我把它稱為“原子性”,但我不確定這是否是正確的術語)。

對於它的價值,關於這個無序方面的Collections.synchronizedList行為是什么?

據我所知,你只需要一個getAndIncrement()方法的變體

public final int getAndIncrement(int modulo) {
    for (;;) {
        int current = atomicInteger.get();
        int next = (current + 1) % modulo;
        if (atomicInteger.compareAndSet(current, next))
            return current;
    }
}

我會說,除了包裝,它沒關系。 當兩個方法調用有效地同時進行時,您無法保證首先會發生哪種方法。

代碼仍然是原子的,因為無論哪個實際發生,它們都不會相互干擾。

基本上,如果您的代碼試圖依賴於同時調用的順序,那么您已經有了競爭條件。 即使在調用代碼中,一個線程在另一個線程之前到達next()調用的開始,你可以想象它在進入 next()調用之前到達其時間片的末尾 - 允許第二個線程到進到那里去。

如果next()調用有任何其他副作用 - 例如它打印出“以thread(thread id)開始” 然后返回下一個值,那么它將不是原子的; 你的行為會有明顯的差異。 事實上,我覺得你很好。

關於包裝的一件事:如果使用AtomicLong你可以在包裝之前使計數器持續更長時間:)

編輯:我剛剛想到了在所有現實場景中避免包裝問題的簡潔方法:

  • 定義一些大數M * 100000(或其他)。 這應該被選擇為足夠大以至於不會經常被擊中(因為它會降低性能)但是足夠小以至於你可以期望下面的“修復”循環在太多線程添加到勾號之前有效包裹。
  • 使用getAndIncrement()獲取值時,請檢查它是否大於此數字。 如果是,進入“減少循環”,看起來像這樣:

     long tmp; while ((tmp = tick.get()) > SAFETY_VALUE)) { long newValue = tmp - SAFETY_VALUE; tick.compareAndSet(tmp, newValue); } 

基本上這說,“我們需要通過遞減模數的一些倍數將值恢復到安全范圍內”(這樣它不會改變模值M的值)。 它在緊密循環中執行此操作,基本上計算出新值應該是什么,但只有在沒有其他任何改變它們之間的值時才進行更改。

它可能會導致病理狀況出現問題,你有無數個線程試圖增加值,但我認為這實際上是可以的。

關於原子性問題:我不相信Counter本身可能提供行為來保證你所暗示的語義。

我想我們有一個線程正在做一些工作

  A - get some stuff (for example receive a message)
  B - prepare to call Counter
  C - Enter Counter <=== counter code is now in control
  D - Increment
  E - return from Counter <==== just about to leave counter's control
  F - application continues

您正在尋找的中介涉及在A處建立的“有效載荷”身份排序。

例如,兩個線程每個讀取一條消息 - 一個讀取X,一個讀取Y.您希望確保X獲得第一個計數器增量,Y獲得第二個,即使兩個線程同時運行,並且可以在1中任意調度或更多CPU。

因此,必須在所有步驟AF中強制執行任何排序,並由計數器外的某些並發控制程序強制執行。 例如:

pre-A - Get a lock on Counter (or other lock)
  A - get some stuff (for example receive a message)
  B - prepare to call Counter
  C - Enter Counter <=== counter code is now in control
  D - Increment
  E - return from Counter <==== just about to leave counter's control
  F - application continues
post- F - release lock

現在我們以犧牲一些並行性為代價來保證; 線程正在等待彼此。 當嚴格排序是一項要求時,這往往會限制並發性; 這是郵件系統中的常見問題。

關於列表問題。 應該從接口保證的角度來看待線程安全性。 絕對最低要求:面對來自多個線程的同時訪問,List必須具有彈性。 例如,我們可以想象一個不安全的列表可能會死鎖或使列表錯誤鏈接,以便任何迭代都會循環。 下一個要求是我們應該在兩個線程同時訪問時指定行為。 有很多案例,這里有一些

a). Two threads attempt to add
b). One thread adds item with key "X", another attempts to delete the item with key "X"
C). One thread is iterating while a second thread is adding

假設實現在每種情況下都有明確定義的行為,它是線程安全的。 有趣的問題是行為方便。

我們可以簡單地在列表上進行同步,因此很容易為a和b提供易於理解的行為。 然而,這在並行性方面是有代價的。 而且我認為沒有任何價值可以做到這一點,因為你仍然需要在更高級別進行同步以獲得有用的語義。 所以我會有一個接口規范說“添加以任何順序發生”。

至於迭代 - 這是一個難題,看看Java集合承諾的內容:不是很多!

本文討論Java集合可能很有趣。

原子 (據我所知)指的是中間狀態不能從外部觀察到的事實。 atomicInteger.incrementAndGet()是原子的,同時return this.intField++; 不是,在前者中,你不能觀察到整數已經增加但尚未返回的狀態。

至於線程安全性 ,Java Concurrency in Practice的作者在他們的書中提供了一個定義:

如果一個類在從多個線程訪問時行為正確,則它是線程安全的,無論運行時環境是否調度或交錯執行這些線程,並且調用代碼沒有額外的同步或其他協調。

(我的個人意見如下)


現在,如果我們已經成功更新了列表,並添加了兩個元素,但是第二個在第一個之前,即最后,是“線程安全”嗎?

如果線程1線程2之前輸入的條目集互斥對象的(在Collections.synchronizedList的情況下()列表本身),可以保證first大於位於前方second在更新后的名單。 這是因為synchronized關鍵字使用公平鎖定。 坐在隊列前面的人首先要做的事情。 公平鎖可能非常昂貴,你也可以在java中使用不公平的鎖(通過使用java.util.concurrent實用程序)。 如果你這樣做,那就沒有這樣的保證。

但是,java平台不是實時計算平台,因此您無法預測一段代碼需要運行多長時間。 這意味着,如果你想要first提前second ,你需要在java中明確地確保這一點。 通過“控制呼叫的時間”來確保這一點是不可能的。

現在,這里的線程安全或不安全是什么? 我認為這完全取決於需要做什么。 如果你只是需要避免列表被破壞而且如果列表中的first一個或second是第一個並不重要,為了使應用程序正確運行,那么只是避免損壞就足以建立線程安全性。 如果沒有,則不然。

所以,我認為在沒有我們試圖實現的特定功能的情況下,無法定義線程安全性。

着名的String.hashCode()不使用java中提供的任何特定的“同步機制”,但它仍然是線程安全的,因為可以在他們自己的應用程序中安全地使用它。 不用擔心同步等

着名的String.hashCode()技巧:

int hash = 0;

int hashCode(){
    int hash = this.hash;
    if(hash==0){
        hash = this.hash = calcHash();
    }
    return hash;
 }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM