在Java 中除了 long 和 double 之外的所有基本類型的讀和賦值,都是原子性操作。而64位的long 和 double 變量由於會被JVM當作兩個分離的32位來進行操作,所以不具有原子性,會產生字撕裂問題。但是當妳定義long或double變量時,如果使用 volatile關鍵字,就會獲到(簡單的賦值與返回操作的)原子性(註意,在Java SE5之前,volatile壹直不能正確的工作)。見第四版《Thinking in java》第21章並發。
volatile關鍵字確保了應用中的可視性。如果妳將壹個域聲明為volatile,那麽只要這個域產生了寫操作,那麽所有的讀操作就都可以看到這個修改。
下面來看看volatile 和原子性的區別和聯系。我將從下面幾個問題進行思考和探索。
第1個問題:如何證明 作者上面所說的long 和 double 的簡單操作是非原子性的 - 會產生字撕裂問題,而使用volatile 關鍵字可以保證 long 或 double 的簡單操作具有原子性,以及驗證其它的基本類型(如int)的簡單操作具有原子性。
我的思路:
1. 多個任務對同壹個 long 變量進行賦值修改,所賦的值為從 1 到64位 僅有1位為1,其余位均為0的數,並所返回賦值完成後的值。如果long 變量不具有原子性,那麽很有可能得到壹個多個位為1的數或者所有位為0的數,壹旦發生,我們輸出壹條信息,並終止程序。
2. 如果1出現字撕裂,那麽long 變量加上 volatile 限制後,賦值返回的數應該都滿足從 1 到64位 僅有1位為1,其余位均為0,即不會出現字撕裂。
3. 同理,測試int變量,但是由於int 賦值具有原子性,所以即使不加 volatile 限制,賦值返回的數應該都滿足從 1 到64位 僅有1位為1,其余位均為0。
具體見下面我寫的測試代碼
// 證明 long 變量簡單操作(賦值和返回)不具有原子性,存在字撕裂問題。驗證 volatile 可確保
// long 變量簡單操作具有原子性。驗證 int 變量簡單操作(賦值和返回)具有原子性
package concurrency;
import java.util.concurrent.*;
class Operation{
private int num = 0;
private long bigNum = 0;
public int assignInt(int n){
num = n;
Thread.yield();
return num;
}
public long assignLong(long n){
bigNum = n;
Thread.yield();
return bigNum;
}
}
public class AtomicTest{
static class IntOperationTask implements Runnable{
private Operation operation;
public IntOperationTask(Operation op){
operation = op;
}
public void run() {
while(true){
int oldNum, newNum;
for(int i = 0; i < 32; i++){
oldNum = 1 << i;
newNum = operation.assignInt(oldNum);
if(oldNum != newNum){
int bits = 0;
for(int j = 0; j < 32; j++){
if(0 != (newNum & (1 << j)))
bits++;
}
if(1 != bits){
System.out.printf("[int TEST] It is no atomic operation." +
" old:x new:xn",oldNum, newNum);
System.exit(0);
}
// else
// System.out.printf("[int TEST] It is no synchronousoperation." +
// " old:x new:xn",oldNum, newNum);
}
}
}
}
}
static class LongOperationTask implements Runnable{
private Operation operation;
public LongOperationTask(Operation op){
operation = op;
}
public void run() {
while(true){
long oldNum, newNum;
long one = 1;
for(int i = 0; i < 64; i++){
oldNum = one << i;
newNum = operation.assignLong(oldNum);
if(oldNum != newNum){
int bits = 0;
for(int j = 0; j < 64; j++){
if(0 != (newNum & (one << j)))
bits++;
}
if(1 != bits){
System.out.printf("[long TEST] It is no atomic operation. " +
"old:6x new:6xn",oldNum, newNum);
System.exit(0);
}
}
}
}
}
}
public static void main(String[] args){
Operation op = new Operation();
ExecutorService service = Executors.newCachedThreadPool();
for(int i = 0; i < 10; i++){
//service.execute(new IntOperationTask(op));
service.execute(new LongOperationTask(op));
}
}
}
測試結果:
1. 當long 沒有使用 volatile 修飾時,不到幾秒,就出現了字撕裂:
[long TEST] It is no atomic operation. old:0000010000000000 new:0000002000000001
[long TEST] It is no atomic operation. old:0000000000040000 new:0000000000000000
[long TEST] It is no atomic operation. old:0000000080000000 new:0000000000000000
[long TEST] It is no atomic operation. old:0000000000100000 new:0000000000000000
[long TEST] It is no atomic operation. old:0010000000000000 new:0000000000000000
[long TEST] It is no atomic operation. old:0000000000000001 new:0000002000000001
[long TEST] It is no atomic operation. old:0001000000000000 new:0000000000000000
[long TEST] It is no atomic operation. old:0001000000000000 new:0000000000000000
[long TEST] It is no atomic operation. old:0000000010000000 new:0000000180000000
上面的測試是在公司的電腦上進行的,可是回到家裏我使用我自己的筆記本電腦進行測試了1分鐘,都沒有出現字撕裂!這是怎麽回事?它吊起了我的興趣!兩臺都 是Win7 64位電腦,都是多核Intel CPU,CPU型號不壹樣,使用的JRE不壹樣,壹個是JRE6(出現字撕裂),壹個是JRE7(運行1分鐘仍未出現字撕裂)。我懷疑是JRE問題,把這 臺電腦的Eclipse 運行環境換成JRE6,還是沒有出現!難道和CPU有關系,這可不好搞,我心裏嘀咕著。冷靜下來,再分析了下,看了下JRE6的路徑是 "C:Program FilesJavajre6" ,我這是Win7 64系統,這意味著我使用的是64位jre環境,會不會我公司用的是32位jre環境?我立即把Eclisep 運行環境換成32位的 jre: "C:Program Files (x86)Javajre6",果然壹運行,就出現字撕裂,這次只打印了壹條,見下面的打印信息。可以觀察到,當使用32位的jre運行 時,javaw.exe 進程是32位進程,但使用64位jre運行時,javaw.exe 進程是64位進程,所以很有可能在64位的jre環境,long double 64位不需要再分離成兩個32位來進行操作,即很有可能它們的賦值操作也是原子性的。
[long TEST] It is no atomic operation. old:4000000000000000 new:0000000000000000
2. 而當long變量使用 volatile 修飾後,程序運行了幾分鐘,也未出現上面的情況。
3. int 變量未使用 volatile 修飾,也未出現字撕裂情況。
第2個問題:作者說在java 中 ++ 操作是非原子性操作,那如果使用++遞增壹個volatile 的int變量,會發生說明,也就是對壹個volatile 的變量進行非原子性操作會發生什麽,會不會像volatile 限定 long double 變量那樣,使得 ++ 變為壹個原子性操作呢?
這個問題《Thinking in java》的作者已給出解答和驗證代碼。當多個任務異步調用 nextSerialNumber 會出現什麽問題呢?
//: concurrency/SerialNumberGenerator.java
package concurrency;
public class SerialNumberGenerator {
private static volatile int serialNumber = 0;
public static int nextSerialNumber() {
return serialNumber++; // Not thread-safe
}
} ///:~
我的思考:
如果 ++ 是原子性操作,那麽由於serialNumber 加上了 volatile 限定,所以任何線程對 serialNumber 的修改,在其它線程都可以看到這個修改。並且 return 壹個 int 也是原子操作,即不會中斷,所以s如果 ++ 是原子性操作,那麽serialNumber在內存的值變化壹定是遞增的(在int 還未溢出為負數時),註意這裏並沒有說返回的值壹定是遞增的,因為可能在++ 完成後,任務就被中斷,其它任務繼續遞增了nextSerialNumber 的值,並返回該值,然後之前那個任務才繼續返回,這樣返回的值就不是遞增的了,但是返回的值在壹定的區間內肯定是不會出現重復的(在int 還未循環回0時)。
如果 ++ 是非原子性操作,那麽有可能有某個任務已經讀取 serialNumber到寄存器了,並在在執行++操作時發生中斷(這個時候serialNumber值還未完成加1,如果是具有則原子性則不會被中 斷),此時另外壹個任務也把serialNumber讀取到寄存器,並執行完++操作後(雖然具有volatile 的限定,但是前面壹個任務已經在此之前讀取了serialNumber,所以也就看不到現在serialNumber修改後的值),前面那個任務才繼續執 行++操作,那麽這兩個任務實際上只對serialNumber完成加1的操作,而不是加2的操作,也就是說這兩次調用返回的值是壹樣的!
通過上面的分析,我們可以斷定,如果++具有原子性,返回的值在壹定的區間內不會發生重復,否則可能會發生重復。
下面是 《Thinking in java 》作者寫的代碼
//: concurrency/SerialNumberChecker.java
// Operations that may seem safe are not,
// when threads are present.
// {Args: 4}
package concurrency;
import java.util.concurrent.*;
// Reuses storage so we don't run out of memory:
class CircularSet {
private int[] array;
private int len;
private int index = 0;
public CircularSet(int size) {
array = new int[size];
len = size;
// Initialize to a value not produced
// by the SerialNumberGenerator:
for(int i = 0; i < size; i++)
array[i] = -1;
}
public synchronized void add(int i) {
array[index] = i;
// Wrap index and write over old elements:
index = ++index % len;
}
public synchronized boolean contains(int val) {
for(int i = 0; i < len; i++)
if(array[i] == val) return true;
return false;
}
}
public class SerialNumberChecker {
private static final int SIZE = 10;
private static CircularSet serials =
new CircularSet(1000);
private static ExecutorService exec =
Executors.newCachedThreadPool();
static class SerialChecker implements Runnable {
public void run() {
while(true) {
int serial =
SerialNumberGenerator.nextSerialNumber();
if(serials.contains(serial)) {
System.out.println("Duplicate: " + serial);
System.exit(0);
}
serials.add(serial);
}
}
}
public static void main(String[] args) throws Exception {
for(int i = 0; i < SIZE; i++)
exec.execute(new SerialChecker());
// Stop after n seconds if there's an argument:
if(args.length > 0) {
TimeUnit.SECONDS.sleep(new Integer(args[0]));
System.out.println("No duplicates detected");
System.exit(0);
}
}
} //:~
結論:當妳定義long或double變量時,如果使用volatile關鍵字限定 long 或 double 變量,就會獲到(簡單的賦值與返回操作的)原子性(註意,在Java SE5之前,volatile壹直不能正確的工作),若沒有使用volatile關鍵字限定,那麽在32位JRE環境下,肯定是非原子性的,在64位 JRE環境下,很有可能具有原子性(上面我的測試是沒有出現字撕裂,呵呵,但我不敢肯定是否壹定具有原子性)。但是如果妳想使++ 遞增操作具有原子性,而僅僅只是同樣使用 volatile 進行限定,那麽妳就會出錯!引用《Thinking in java》作者的話:原子操作就是不能被線程調度機制中斷的操作。不正確的認識:原子操作不需要進行同步。volatile關鍵字確保了應用中的可視性。 如果妳將壹個域聲明為volatile,那麽只要這個域產生了寫操作,那麽所有的讀操作就都可以看到這個修改。