Java中原子操作是線程安全的論調經常被提到。根據定義,原子操作是不會被打斷的操作,因此被認為是線程安全的。實際上有一些原子操作不一定是線程安全的。
<!-- frame contents -->
<!-- /frame contents -->
這個問題出現的原因是盡量減少在代碼中同步要害字。同步會損害性能,雖然這個損失因JVM不同而不同。另外,在現代的JVM中,同步的性能正在逐步提高。盡管如此,使用同步仍然是有性能代價的,並且程序員永遠會盡力提高他們的代碼的效率,因此這個問題就延續了下來。
在java中,32位或者更少位數的賦值是原子的。在一個32位的硬件平台上,除了double和long型的其它原始類型通常都是使用32位進行表示,而double和long通常使用64位表示。另外,對象引用使用本機指針實現,通常也是32位的。對這些32位的類型的操作是原子的。
這些原始類型通常使用32位或者64位表示,這又引入了另一個小小的神話:原始類型的大小是由語言保證的。這是不對的。java語言保證的是原始類型的表數范圍而非JVM中的存儲大小。因此,int型總是有相同的表數范圍。在一個JVM上可能使用32位實現,而在另一個JVM上可能是64位的。在此再次強調:在所有平台上被保證的是表數范圍,32位以及更小的值的操作是原子的。
那麼,原子操作在什麼情況下不是線程安全的?主要的一點是他們也許確實是線程安全的,但是這沒有被保證!java線程答應線程在自己的內存區保存變量的副本。答應線程使用本地的私有拷貝進行工作而非每次都使用主存的值是為了提高性能。考慮下面的類:
class RealTimeClock
{
private int clkID;
public int clockID()
{
return clkID;
}
public void setClockID(int id)
{
clkID = id;
}
//...
}
現在考慮RealTimeClock的一個實例以及兩個線程同時調用setClockID和clockID,並發生以下的事件序列:
T1 調用setClockID(5)
T1將5放入自己的私有工作內存
T2調用setClockID(10)
T2將10放入自己的私有工作內存
T1調用clockID,它返回5
5是從T1的私有工作內存返回的
對clockI的調用應該返回10,因為這是被T2設置的,然而返回的是5,因為讀寫操作是對私有工作內存的而非主存。賦值操作當然是原子的,但是因為JVM答應這種行為,因此線程安全不是一定的,同時,JVM的這種行為也不是被保證的。
兩個線程擁有自己的私有拷貝而不和主存一致。假如這種行為出現,那麼私有本機變量和主存一致必須在以下兩個條件下:
1、變量使用volatile聲明
2、被訪問的變量處於同步方法或者同步塊中
假如變量被聲明為volatile,在每次訪問時都會和主存一致。這個一致性是由java語言保證的,並且是原子的,即使是64位的值。(注重很多JVM沒有正確的實現volatile要害字。你可以在www.javasoft.com找到更多的信息。)另外,假如變量在同步方法或者同步塊中被訪問,當在方法或者塊的入口處獲得鎖以及方法或者塊退出時釋放鎖是變量被同步。
使用任何一種方法都可以保證ClockID返回10,也就是正確的值。變量訪問的頻度不同則你的選擇的性能不同。假如你更新很多變量,那麼使用volatile可能比使用同步更慢。記住,假如變量被聲明為volatile,那麼在每次訪問時都會和主存一致。與此對照,使用同步時,變量只在獲得鎖和釋放鎖的時候和主存一致。但是同步使得代碼有較少的並發性。
假如你更新很多變量並且不想有每次訪問都和主存進行同步的損失或者你因為其它的原因想排除並發性時可以考慮使用同步。