被volatile 修飾的變量有以下如下重要特點:在引用這種變量的線程之間,被修飾的變量能夠保證其“可見性”。
也就是說,假如X線程修改了a,這條數據會立即更新到內存區域中,Y線程再讀取的時候也是從內存中讀取,會讀取到a變量更新後的值。反過來,假如a變量不用volatile修飾,那麼當X修改了a變量時,有可能僅僅將a修改後的值保存在了catch中,此時新的a值對X可見對Y不可見,Y值有可能會獲取a的“髒數據”。
說清了這個概念,再看看下面的代碼。功能是兩個線程同時對a進行 +1操作,各加10000遍。
public class IntegerCountTest {
static volatile Integer a = 0;
static class MyTask implements Runnable{
CountDownLatch ct;
public MyTask( CountDownLatch ct2) {
this.ct = ct2;
}
@Override
public void run() {
for(int i=0;i<10000;i++) {
a = a+1;
}
this.ct.countDown();
}
}
public static void main(String[] arg) {
CountDownLatch ct = new CountDownLatch(2);
Thread t1 = new Thread(new MyTask(ct));
Thread t2 = new Thread(new MyTask(ct));
t1.start();
t2.start();
try {
ct.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(a);
}
}
按直覺將,最終輸出的a應該是20000,但實際a 是一個趨近於20000的隨機數。既然 a被volatile 修飾,能夠保證 在t1 和 t2之間的可見性,那麼爲什麼還會出現這種情況?
因爲 a = a+1;不是一個原子操作。
這行代碼至少會作2個操作,先是獲取a的值,加1生成新的值再賦值給a變量。可能t1 和t2同時運行到 a = a+1這一行,同時獲取了a的舊值x ,然後先後將同一個x+1的值賦值給了x。這樣就導致了這種情況。
怎麼解決?將a = a+1變爲一個原子操作,如下所示,將a定義成AtomicInteger 變量,其中a.incrementAndGet();操作能夠保證+1操作的原子性。
public class IntegerCountTest {
//static volatile Integer a = 0;
//static Integer a = 0;
static AtomicInteger a = new AtomicInteger(0);
// private static sun.misc.Unsafe UNSAFE ;
// private static long aOffset ;
// static {
// UNSAFE = sun.misc.Unsafe.getUnsafe();
// Class<?> k = IntegerCountTest.class;
// try {
// aOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("a"));
// } catch (NoSuchFieldException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// } catch (SecurityException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// }
static class MyTask implements Runnable{
CountDownLatch ct;
public MyTask( CountDownLatch ct2) {
this.ct = ct2;
}
@Override
public void run() {
for(int i=0;i<10000;i++) {
//a = a+1;
// for(;;) {
// int temp = a;
// int newvar = temp+1;
// if(UNSAFE.compareAndSwapInt(this, aOffset, newvar, temp)) {
// break;
// }
// }
a.incrementAndGet();
}
this.ct.countDown();
}
}
public static void main(String[] arg) {
CountDownLatch ct = new CountDownLatch(2);
Thread t1 = new Thread(new MyTask(ct));
Thread t2 = new Thread(new MyTask(ct));
t1.start();
t2.start();
try {
ct.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(a);
}
}
當然,還有其他很多方法可以實現,比如加鎖,以及jvm內部使用的UNSAFE方法等。