1、先看一段代碼

public class T {

boolean isRunning = true;

public void m() {
System.out.println("m start...");
while(isRunning) {

}
System.out.println("m end...");
}

public static void main(String[] args) {
T t = new T();
new Thread(t::m, "t1").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.isRunning = false;
}

}

  • 首先新建一個線程t1啟動,調用m方法,進入死循環,按道理來說我主線程執行到下面的時候把isRunning改成false就會結束t1線程中的死循環,但是執行你會發現並不會結束。
  • 這是因為每個線程運行都會有自己的一份內存也可以叫做緩衝區,比如下面這張圖,上面一個t1線程和主線程分別佔用一個CPU,那麼CPU中會有個對應線程的緩衝區。
  • 代碼執行過程:棧內存,堆內存,方法區之類的內存稱之為主內存,堆內存對應的t對象中有個成員變數isRunning=true,在t1線程啟動的時候會從主內存中copy一份isRunning(boolean類型為一個位元組)的值到自己的緩衝區中,然後就開始死循環一直執行下去,這時候主線程把isRunning的值copy到自己的緩衝區然後執行isRunning=false並且寫會主內存中去,但是t1線程是非常繁忙的,一直在執行死循環沒有時間去主內存刷新自己isRunning的值,所以就算主線程修改了isRunning的值,t1線程還是死循環
  • 解決方法:在isRunning成員變數前面加上volatile關鍵字
    • volatile關鍵字的作用:就是保持線程的可見性,只要該變數的值修改了,就會通知其他線程你們緩衝區中的copy過期了,需要重新來主內存中刷新一下,這時候t1線程就會得到通知,並且來主內存中刷新isRunning的值,從而停止死循環。注意:使用volatile的作用不是t1線程會每次使用到isRunning的時候都去主內存中讀一下isRunning的值,而是主內存中的isRunning被修改了,就會通知所有的使用到isRunning的線程來主內存中刷新一下isRunning的值
    • 也可以在死循環中sleep一段時間,或者列印一些語句,使CPU不要太忙,得出空閑就會來主內存中刷新isRunning的值,但是這樣是不穩定的,我怎麼知道你CPU什麼時候空閑,所以該用volatile還是得用volatile

2、再來看一段代碼

public class T {

volatile int count = 0;

public void m() {
for (int i = 0; i < 10000; i++) {
count++;
}
}

public static void main(String[] args) {
T t = new T();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
threads.forEach(o -> o.start());
threads.forEach(o -> {
try {
//等待所有線程結束
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

System.out.println(t.count);
}
}

  • 按照正常的思路列印出來應該是100000,但是列印出來的結果會遠遠小於100000,因為volatile只保證可見性不保證原子性,而count++不是原子性操作,分為拿到count的值,然後加1,再賦值。這時候如果有兩個線程同時拿到count=100到緩衝區中,加1再寫入主內存中。這時候兩個線程合起來只增加了1,所以最終會少很多。這裡保證的可見性,只保證線程拿到的值一定是最新的。
  • 解決方法:
    • 成員變數上不加volatile,用synchronized修飾m方法,或者用synchronized同步代碼塊包裹住count++
    • 或者使用如果僅僅只是一些自增自減,判斷是否為true等之類的操作,沒必要使用synchronized來保證原子性,Java中提供了很AtomicXXX類,原子操作的類,就是該類中的所有方法都是原子性的。不是使用synchronized保證原子性,使用的是很底層的方式,比synchronized效率要高。但是該類兩個方法同時使用時不能保證原子性(在兩個方法調用的之間還是有可能會被別的線程搶佔進入)

public class T {

AtomicInteger count = new AtomicInteger(0);

public void m() {
for (int i = 0; i < 10000; i++) {
count.incrementAndGet();
}
}

public static void main(String[] args) {
T t = new T();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
threads.forEach(o -> o.start());
threads.forEach(o -> {
try {
//等待所有線程結束
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}

結論:volatile只保證線程的可見性,synchronized既保證可見性也保證原子性,但是synchronized相對於volatile效率會低很多,所以能用volatile替代synchronized,就盡量使用volatile。


推薦閱讀:
相關文章