天天看點

原子性和可見性有序性

原子性:

原子性就是說一個操作不可以被中途cpu暫停然後排程, 即不能被中斷, 要不就執行完, 要不就不執行. 如果一個操作是原子性的, 那麼在多線程環境下, 就不會出現變量被修改等奇怪的問題.

原子性的操作是線程安全的。非原子性操作是線程不安全的(也就是說隻有簡單的讀取,指派操作是原子性操作–(而且必須是将數字指派給某個變量,變量之間的互相指派不是原子操作))

boolean a = true;

if(a){

a=false;

}

其中 讀取a的值,和對a指派的操作都是原子性操作

可見性:

Java提供了volatile關鍵字來保證可見性。當一個變量被volatile修飾時再他的值被修改之後會立即重新整理進主存當中,在其他線程讀取這個變量時也需要到主存當中讀取。volatile保證一個線程修改的值對其他線程是立即可見的。(另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻隻有一個線程擷取鎖然後執行同步代碼,并且在釋放鎖之前會将對變量的修改重新整理到主存當中。是以可以保證可見性。)

常見的一段代碼:

線程1:

boolean state = false;

while(!state) {

doSth();

}

線程2:

state = true;

線程1先執行,線程2後執行(這是我們經常用來終止一個線程的操作)。但是當線程2執行之後線程1會立即停止嗎?由于線程2修改state之後對線程1并不是立即可見的。是以線程1不一定會立即停止(在大多數時候線程1是能夠終止的。)因為每個線程都有自己的工作記憶體線程2修改值之後對線程1并不是立即可見的。是以不一定立即停止。(上面的代碼如果給state加上volatile關鍵字-那麼線程1,2中的工作記憶體中的state是無效的。對state的簡單指派操作是直接修改主存。讀操作也是每次從主存中讀取。)

有序性:

int a = 10; //語句1

int r = 2; //語句2

a = a + 3; //語句3

r = a*a; //語句4

java在運作的過程當中無法保證語句1,和語句2誰先運作,誰後運作。但是可以保證在一個線程裡語句1和語句2的先後順序對最後的執行結果沒有影響(但是可以通過依賴保證語語句3一定在語句1之後執行,語句4一定在語句3之後執行)

//線程1:

context = loadContext(); //語句1

inited = true; //語句2

//線程2:

while(!inited ){

sleep()

}

doSomethingwithconfig(context);

但是在多個線程裡就無法保證了。上面代碼裡很有可能語句2執行在語句1之前。這樣的話當context還有初始化,線程2就跳出循環使用context了。這樣就不安全了。

這時候可以使用volatile關鍵字。volatile對 inited進行修飾,就可以保證在執行語句2的時候,語句2上面的代碼全部執行完了。且語句2下面的代碼一個都沒有執行。但是語句2上面和語句2下面代碼的順序不保證。

//x、y為非volatile變量

//flag為volatile變量

x = 2; //語句1

y = 0; //語句2

flag = true; //語句3

x = 4; //語句4

y = -1; //語句5

由于flag變量為volatile變量,那麼在進行指令重排序的過程的時候,不會将語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5後面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。

  并且volatile關鍵字能保證,執行到語句3時,語句1和語句2必定是執行完畢了的,且語句1和語句2的執行結果對語句3、語句4、語句5是可見的。

看一段常見的代碼:

public class Test {

public volatile int inc = 0;

public void increase() {
    inc++;
}

public static void main(String[] args) {
    final Test test = new Test();
    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);
}
           

}

這段代碼每次執行的結果不一樣,但是總是小于10000的

(每次讀的都是主存中的沒錯,但是自增操作不是原子性的,是可以被打斷的。當一個線程中的增加操作暫停了。另一個線程可能又去主存讀了。這時候就可能導緻2個自增操作。最後結果隻加了1.)

可能有的朋友就會有疑問,不對啊,上面是對變量inc進行自增操作,由于volatile保證了可見性,那麼在每個線程中對inc自增完之後,在其他線程中都能看到修改後的值啊,是以有10個線程分别進行了1000次操作,那麼最終inc的值應該是1000*10=10000。

  這裡面就有一個誤區了,volatile關鍵字能保證可見性沒有錯,但是上面的程式錯在沒能保證原子性。可見性隻能保證每次讀取的是最新的值,但是volatile沒辦法保證對變量的操作的原子性。

  在前面已經提到過,自增操作是不具備原子性的,它包括讀取變量的原始值、進行加1操作、寫入工作記憶體。那麼就是說自增操作的三個子操作可能會分割開執行,就有可能導緻下面這種情況出現:

  假如某個時刻變量inc的值為10,

  線程1對變量進行自增操作,線程1先讀取了變量inc的原始值,然後線程1被阻塞了;

  然後線程2對變量進行自增操作,線程2也去讀取變量inc的原始值,由于線程1隻是對變量inc進行讀取操作,而沒有對變量進行修改操作,是以不會導緻線程2的工作記憶體中緩存變量inc的緩存行無效,是以線程2會直接去主存讀取inc的值,發現inc的值時10,然後進行加1操作,并把11寫入工作記憶體,最後寫入主存。

  然後線程1接着進行加1操作,由于已經讀取了inc的值,注意此時線上程1的工作記憶體中inc的值仍然為10,是以線程1對inc進行加1操作後inc的值為11,然後将11寫入工作記憶體,最後寫入主存。

  那麼兩個線程分别進行了一次自增操作後,inc隻增加了1。

  解釋到這裡,可能有朋友會有疑問,不對啊,前面不是保證一個變量在修改volatile變量時,會讓緩存行無效嗎?然後其他線程去讀就會讀到新的值,對,這個沒錯。這個就是上面的happens-before規則中的volatile變量規則,但是要注意,線程1對變量進行讀取操作之後,被阻塞了的話,并沒有對inc值進行修改。然後雖然volatile能保證線程2對變量inc的值讀取是從記憶體中讀取的,但是線程1沒有進行修改,是以線程2根本就不會看到修改的值。

  根源就在這裡,自增操作不是原子性操作,而且volatile也無法保證對變量的任何操作都是原子性的。

可以修改為下面幾種寫法:

采用synchronized:

public class Test {

public int inc = 0;

public synchronized void increase() {
    inc++;
}

public static void main(String[] args) {
    final Test test = new Test();
    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:

public class Test {

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 Test test = new Test();
    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);
}
           

}

  采用AtomicInteger:

  public class Test {

public AtomicInteger inc = new AtomicInteger();

public  void increase() {
    inc.getAndIncrement();
}

public static void main(String[] args) {
    final Test test = new Test();
    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);
}
           

}

轉:http://www.cnblogs.com/dolphin0520/p/3920373.html

繼續閱讀