深入volatile關鍵字

  • 2020 年 1 月 21 日
  • 筆記

在Java多執行緒中,有一個特殊的關鍵字volatile,這個通常成為一個「輕量級鎖」,下面我們就來深入的了解這個關鍵的作用和原理。

執行緒的記憶體備份

首先看一段程式碼:

	public class VolatileThread extends Thread {  		private  boolean isRuning=true;  		private void setRuning(boolean runing){  			this.isRuning=runing;  		}    		public void run(){  			System.out.println("進入Run方法");  			while (isRuning){    			}  			System.out.println("執行緒結束");  		}    		public static void main(String[] args) throws InterruptedException {  			VolatileThread volatileThread = new VolatileThread();  			volatileThread.start();  			Thread.sleep(3000);  			volatileThread.setRuning(false);  			System.out.println("runing設置成false,讓執行緒停止");  			Thread.sleep(1000);  			System.out.println(volatileThread.isRuning);  		}  	}

在上面的程式碼並沒有列印出「執行緒結束」的資訊,因為我在主執行緒更改了isRuning 的值,並沒有影響到執行緒中的數據。

產生這個的原因是因為JDK在創建執行緒的時候,都會從主記憶體中拷貝一份數據,所以執行緒的讀取的變數的具有一定延遲

使用volatile

對上面的程式碼進行修改,把isRuning變數使用volatile 關鍵字修飾,這樣我們就能看到執行緒能夠正常的停止了。下面我們總結下volatile的作用

  如果變數被volatile關鍵字修飾,  則當變數改變的時候強制執行緒從主記憶體中讀取和寫入變數

CPU的快取怎麼辦

程式碼最終的是由CPU執行的,為了保證CPU的執行效率,在讀取數據的時候,CPU是優先把數據快取到自己的高速快取中,高速快取帶來了效率上面的提高,也同樣帶來了數據一致性的問題。

例如下面這一段簡單的程式碼:

count++;

當程式運行的時候,count會被拷貝到CPU高速快取中,知道執行結束才會重新刷到主記憶體中。

如果在多執行緒的環境中,就會出現數據不一致的問題。

解決這個問題的方法有兩種:

  1. 在匯流排的位置加鎖,一次只允許一個CPU訪問記憶體。
  2. 使用快取一致性協議,當CPU發現當前的變數是volatile變數,就會被告知通知其他CPU告訴該變數的快取無效,這樣CPU就會從記憶體中重新載入數據

volatile 不具備原子性

共享變數只是在讀和寫的時候具有原子性,但是複雜的count++運算不具備原子性。

public class AppTest {  	private volatile int count = 0;    	public static void main(String[] args) throws InterruptedException {    		AppTest app = new AppTest();    		for (int i = 0; i < 10000; i++) {  			Thread thread = new Thread(() -> {  				for (int m = 0; m < 1000; m++) {  					app.count();  				}  			});  			thread.start();  		}  		System.out.println(app.count);  		for (int i=0;i<10;i++){  			Thread.sleep(1000);  			System.out.println(app.count);  		}  	}    	private void count() {  		count++;  	}  }

列印結果:

9444777  9578523  9578523

看到最終結果不是10000000。