[英]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_VALUE
, next()
可能會返回錯誤的值,具體取決於模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, 1
對next()
兩次調用將返回1, 1
。
next()
獲取無序值也可能存在問題,例如:
next()
next()
tick.getAndIncrement()
,返回x tick.getAndIncrement()
返回Y = X + 1(mod M)表示 在這里,除了前面提到的包裝問題, x和y確實是這兩個next()
調用返回的兩個正確值,但是根據指定計數器行為的方式,可以認為它們是亂序的。 也就是說,我們現在有(Thread1,y)和(Thread2,x) ,但是可能真的應該指定(Thread1, x)和(Thread2,y)是“正確的”行為。
因此,通過對單詞的一些定義, AtomicModularCounter
是線程安全的 ,但實際上不是原子的 。
所以問題是:
AtomicInteger
的原子性,你能不使用synchronized
修復它嗎? tick
本身由模數控制范圍,甚至沒有機會包裹Integer.MAX_VALUE
?
M
至少是一個小於Integer.MAX_VALUE
的訂單 這是List
無序“問題”的類比。
add(first)
add(second)
現在,如果我們已經成功更新了列表,並添加了兩個元素,但是second
在first
之前,即最后,是“線程安全”嗎?
如果那是“線程安全的”,那么它不是什么? 也就是說,如果我們在上面的場景中指定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
你可以在包裝之前使計數器持續更長時間:)
編輯:我剛剛想到了在所有現實場景中避免包裝問題的簡潔方法:
使用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.