volatile關鍵字的介紹和使用
關鍵字volatile的主要作用是使變數在過個執行緒中可見
1、假設volatile不存在我們將會面對的問題
public class PrintString implements Runnable
{
private boolean isContinuePrint = true;
public boolean isContinuePrint() {
return isContinuePrint;
}
public void setContinuePrint(boolean isContinuePrint) {
this .isContinuePrint = isContinuePrint;
}
public void printStringMethod()
{
try
{
while(isContinuePrint)
{
System.out.println("run printStringMethod threadName = " + Thread.currentThread().getName());
Thread.sleep(1000);
}
}
catch (InterruptedException exception)
{
exception.printStackTrace();
}
}
@Override
public void run() {
printStringMethod();
}
}
public class Test {
/**
* @param args
*/
public static void main(String[] args)
{
try
{
PrintString mPrintString = new PrintString();
Thread mThread = new Thread(mPrintString);
mThread.start();
Thread.sleep(1000);
mPrintString.setContinuePrint(false);//在-server模式下,這個操作修改的是公共堆疊中的值,而執行緒的私有堆疊並沒有修改
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
當JVM 在-server模式下並不會停止PrintString的method,因為在-server的模式下存在一個公共堆疊和執行緒私有堆疊的概念。我們在呼叫setConinuePrinter(false)修改的是公共堆疊中的值,並不會影響到執行緒的私有堆疊。
此時我們需要引入volatile關鍵字,作用是強制執行緒去公共堆疊中訪問isContinuePrint的值。
使用volatile關鍵字增加了例項變數在多個執行緒之間的可見性,但volatile關鍵字有一個致命的缺陷是不支援原子性
synchronized與volatile關鍵字之間的比較
- 關鍵字volatile是執行緒同步的輕量實現,所以volatile關鍵字效能比synchronized好。volatile只能修飾變數,synchronized可以修飾方法,程式碼塊
- volatile不會阻塞執行緒,synchronized會阻塞執行緒
- volatile能保證資料的可見性,不保證原子性,synchronized可以保證原子性,可以間接保證可見性,它會將公共記憶體和私有記憶體的資料做同步處理。
- volatile解決的是變數在多個執行緒之間的可見性,synchronized解決的是多個執行緒之間訪問資源的同步性
請記住Java的同步機制都是圍繞兩點:原子性,執行緒之間的可見性.只有滿足了這兩點才能稱得上是同步的。Java中的synchronized和volatile兩個關鍵字分別執行的是原子性和執行緒之間的可見性。
volatile 並非原子性
看一個例子
package com.sophia.demo;
public class MyThread extends Thread
{
volatile public static int COUNT;
private static void addCount()
{
for (int i = 0; i < 100; i++)
{
COUNT++;
}
System.out.println("count = " + COUNT);
}
@Override
public void run()
{
addCount();
}
}
package com.sophia.demo;
public class StringTest {
public static void main(String[] args)
{
MyThread[] myThreads = new MyThread[100];
for (int i = 0; i < myThreads.length; i++)
{
myThreads[i] = new MyThread();
}
for (MyThread myThread : myThreads)
{
if (null != myThread)
{
myThread.start();
}
}
}
}
輸出的結果並不如我們想象中的那樣子遞增
count = 200
count = 300
count = 200
仔細觀察輸出結果中我們看到有這樣子一段結果,當我們在addCount()方法中加上synchronized方法就能達到同步的效果。此時就沒必要使用volatile關鍵字了。
首先我們來分析一個常見的表示式i++即i = i + 1;這個操作不是原子性也就是非執行緒安全的,分解的步驟如下
- 從記憶體中取出i的值
- 計算i的值
- 將i的值寫入記憶體中
假設在執行第2步的時候,有其他執行緒在修改i的值,這個時候就會出現髒資料,解決的辦法就是使用synchronized關鍵字。所以說volatile並不處理資料的原子性
詳細解釋一下volatile出現執行緒不安全的原因:
- read和load過程:從主存中複製變數到當前執行緒的工作記憶體中
- use和assign過程:執行程式碼,更改變數的值
- store和write過程:儲存更改後的變數,並寫入主存。
在多執行緒換種中,use和assign是多次出現的,但這一操作並不是原子性,也就是在read和load之後,如果主記憶體count變數傳送修改之後,執行緒工作記憶體中的值由於已經載入,不會產生對應的變化,也就是私有記憶體和公共記憶體的值不一樣。此時其他執行緒在去取公共記憶體的值就會造成偏差。導致執行緒不安全。所以多個執行緒訪問同一個變數加鎖才是最安全的操作。
Java提供了一個執行緒安全的Integer類,AtomicInteger,原子操作是不可分割的整體,沒有其他執行緒能夠終端或檢查正在原子操作中的變數,可以在沒有鎖的情況下做到執行緒安全