1. 程式人生 > >volatile關鍵字解析(二)

volatile關鍵字解析(二)

禁止 new incr lock 解析 static style ron running

volatile詳解
接下來,我們詳細講述一下volatile關鍵字
volatile關鍵字具有兩重語義

  • 保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這個新值對其他線程來說是立即可見的。
  • 禁止指令重排序

依然使用上文RunThread案例說明,上文中已經提到了,由於主線程修改了isRunning的值後,RunThread線程並沒有看到其值的變化,因此,進入了死循環,但是當isRunning加上volatile關鍵字後,效果就不一樣了。

  1. 使用volatile關鍵字會強制將修改的值立即寫入主存;
  2. 使用volatile關鍵字的話,當主線程修改時,會導致RunThread的工作內存中isRunning變量的緩存值變得無效。
  3. 由於RunThread的工作內存中緩存變量isRunning緩存無效,所以會再次從主存中讀取isRunning變量值。

這樣就保證了RunThread可以順利結束。


關於原子性,我們先看個例子:

public class TestCount
{
    private volatile int n;
    
    public void increase()
    {
        n ++;
    }
    
    public static void main(String[] args)
    {
        TestCount count = new TestCount();
        
for(int i=0; i<10; i++) { new Thread(){ public void run() { for(int j=0;j<1000;j++) count.increase(); }; }.start(); } if(Thread.activeCount() > 1) { Thread.yield(); } System.out.println(count.n); } }

關於這個結果,應該很多人會認為是10000吧。但是實際上,運行結果小於10000,這是由於n++操作並非原子性操作,其涉及三個操作

  1. 讀出n的舊知
  2. 對n+1
  3. 寫入新值

雖然volatile可以保證每個線程讀取到的是最新的內存值,但是,如果10個線程第一步都是正確的,第二步和第三步單線程看的話也都是沒有問題的,但是如果並發的話,就會導致同一個值被多次寫入了內存。
可見volatile並不能保證原子性操作。
對於這一類問題,我們可以通過synchronized、lock或者原子操作類經行操作。
synchronized版本:

public class TestCount
{
    public  int inc = 0;
    
    public synchronized void increase() {
        inc++;
    }
 
    public static void main(String[] args) {
        final TestCount test = new TestCount();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
 
        while(Thread.activeCount()>1)  //保證前面的線程都執行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

lock版本:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestCount
{
    public  int inc = 0;
    Lock lock = new ReentrantLock();
 
    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }
 
    public static void main(String[] args) {
        final TestCount test = new TestCount();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
 
        while(Thread.activeCount()>1)  //保證前面的線程都執行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

原子操作類版本:

import java.util.concurrent.atomic.AtomicInteger;

public class TestCount
{
    public  AtomicInteger inc = new AtomicInteger();
    
    public  void increase() {
        inc.getAndIncrement();
    }
 
    public static void main(String[] args) {
        final TestCount test = new TestCount();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
 
        while(Thread.activeCount()>1)  //保證前面的線程都執行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

對於有序性,我們可以看下下面的這個例子:

//線程1:
context = loadContext();   //語句1
inited = true;             //語句2
     
//線程2:
while(!inited ){
    sleep()
}
doSomethingwithconfig(context);

如果沒有對inited加volatile關鍵字,則可能會對語句1和語句2進行調整,因此兩者並無依賴關系,並且單線程下,也並不會影響結果。但是兩個線程下,可能由於context並沒有初始化完成,導致意想不到的結果。
如果對inited加上volatile,這個問題就不會發生了。語句1和語句2的執行順序則不會被調整。

volatile關鍵字解析(二)