可獲得兩大新人禮包
36份一線互聯網Java面試電子書
84個Java稀缺面試題視頻
如果我現在問你volatile的關鍵字的作用,你可能會回答對於一個線程修改的變量對其他的線程立即可見。這種說法沒多大問題,但是不夠嚴謹。
嚴謹的回答應該是volatile關鍵字對於基本類型的修改可以在隨後對多個線程的讀保持一致,但是對於引用類型如數組,實體bean,僅僅保證引用的可見性,但並不保證引用內容的可見性。
下面這些數據結構都屬於引用類型,即使使用volatile關鍵字修飾,也不能保證修改後的數據會立即對其他的多個線程保持一致:
Java代碼
```java
volatile int [] data;
valatile boolean [] flags;
volatile Person person;
```
如何證明?看下面的一段代碼:
Java代碼
```java
private static volatile Data data;
public static void setData(int a, int b) {
data = new Data(a, b);
}
private static class Data {
private int a;
private int b;
public Data(int a, int b) {
this.a = a;
this.b = b;
}
public int getA() {
return a;
}
public int getB() {
return b;
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000; i++) {
int a = i;
int b = i;
//writer
Thread writerThread = new Thread(() -> {setData(a, b);});
//reader
Thread readerThread = new Thread(() -> {
while (data == null) {}
int x = data.getA();
int y = data.getB();
if (x != y) {
System.out.printf("a = %s, b = %s%n", x, y);
}
});
writerThread.start();
readerThread.start();
writerThread.join();
readerThread.join();
}
System.out.println("finished");
}
```
在我的mac系統上,運行了第三次的時候出現了不一致:
Java代碼
```
a = 2760, b = 2761
a = 3586, b = 3587
finished
```
原因是對於屬性a和b我們都是分別的讀取,所以缺乏了happens-before關係的約束。
如何解決這種情況?
(1)去掉獨立的getA和getB方法,使用int數組,一次返回兩個屬性
Java代碼
```
public int[] getValues() {
return new int[]{a, b};
}
```
(2)使用java併發包下面的基於CAS的原子結構:
AtomicReference
Java代碼
```
//修改1
private static AtomicReference data = new AtomicReference<>();
//修改2
public static void setData(int a, int b) {
data.compareAndSet(null, new Data(a, b));
}
//修改3
Thread readerThread =
new Thread(() -> {while (data.get() == null) {}
int x = data.get().getA();
int y = data.get().getB();
if (x != y) {
System.out.printf("a = %s, b = %s%n", x, y);
}
});
```
總結:
本篇文章主要講述了關於volatile修飾引用變量的問題即它只能保證引用本身的可見性,並不能保證內部字段的可見性,如果想要保證內部字段的可見性最好使用CAS的數據結構,這裡還需要說明的的一點是volatile有時候修飾引用類型如boolean數組可能結果是沒問題的。
在編程的世界裡面,對於不確定的事情,我們始終都要以最壞的打算來看待,所以請記住:儘量避免使用volatile關鍵字修飾引用變量。
閱讀更多 李紅 的文章