2017年10月31日 星期二

volatile 和 原子性

※volatile


尤於 CPU 的速度比主存還快,所以每個 Thread 都會有一個專屬的 cache,將主存的資料從主存拷過去

volatile 能保證 1.內存可見性和 2.排序性
.volatile 只能宣告在全域變數
.內存可見性:volatile 有揮發性的意思,意思就是用完就丟,在 java 裡的意思是值有改變就去主存抓
以上面的圖來說,要是值改變了,就相當於沒有 CPU cache 了,所以會到主存去抓 (保下面的賣票例子不可用 volatile,因為沒有原子性)

.排序性:CPU 會將我們寫的程式碼重排,如寫在第一行,但不一定就是先執行,不過他保證結果是一樣的,但可惜只是單線程一樣,多線程有可能會不一樣,而加上這個關鍵字可以禁止 CPU 重排

.只能解決一寫多讀的情形

.++i 是原子性;i++ 不是原子性,可用 AtomicXXX 或 LongAdder 產生原子性,高併發時用 LongAdder 較快,否則 AtomicXXX 較快



public class App {
    private volatile boolean flag = false;
    
    public void xxx() {
        new Thread(() -> {
            try {
                Thread.sleep(200);
                flag = true;
                System.out.println("oooooooooooooo");
            } catch (InterruptedException e) {
            }
        }).start();
    
        new Thread(() -> {
            while (true) {
                if (flag) {
                    System.out.println("xxxxxxxxxxxxxx");
                    break;
                }
            }
        }).start();
    }
    
    public static void main(String... ss) {
        new App.xxx();
    }
}


※此例如果不加 volatile 有可能兩個 CPU 同時抓到,所以迴圈裡的 flag 永遠都是 false了

※此例也可用 synchronized 或 Lock,但效能比較差

※不代表 volatile 可取代 synchronized
volatile 並沒有互斥性,也不能保證原子性
互斥性:像 synchronized 和 Lock 就有,一次只能一個進入,但指的是相同的物件
例:HttpSession 可以鎖定成功;但 HttpServletRequest 就不可能鎖定成功


※賣票(必需要有互斥性)

public class AppTest {
    volatile AtomicInteger ticket = new AtomicInteger(10);
    // Integer ticket = 10;
    
    public static void main(String... s) {
        new AppTest().xxx();
    }
    
    public void xxx() {
        Runnable run = () -> {
            for (;;) {
                // if (ticket > 0) {
                System.out.println("票" + ticket.get());
                if (ticket.get() > 0) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        // System.out.println(--ticket);
                        System.out.println(ticket.decrementAndGet());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        };
    
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
}


※還沒加原子性:雖然在減 1 時,其他線程有看見,但有可能還沒寫,其他線程就讀到舊的,因為 volatile 沒有原子性

※加原子性的 AtomicInteger 後,使用兩個線程看起來好像可以,實際上還是有問題,因為此例是偶數票,每次少兩張,最後是 0,但用更多的線程去跑或改成單數票又不行了

.因為雖然加了原子性,但在改之前,其他線程還是讀的到 (資料庫的不可重覆讀)
如第 1 次,四個線程都讀到 10,然後都減 1,但有原子性,所以最後是 6
而第 2 次,四個線程都讀到  6,然後都減 1,還是有原子性,所以最後是 2
而第 3 次,四個線程都讀到  2,然後都減 1,還是有原子性,所以最後是 -2
也就是說一次只能有一個線程讀取,所以必需要有互斥性

如果沒有 if,只讓它一直加或一直減,最終會是正確的



※原子性

表示不能切割,如後置遞增/減
例:i++ 在底層運作是
int temp = i;
i = i+1;
return temp;
這三步是不能切割的,但 volatile 無法保證這一點



public class MyThread implements Runnable {
    private int increment;
    // private AtomicInteger increment = new AtomicInteger(0);
    
    @Override
    public void run() {
        try {
            Thread.sleep(200);
            System.out.println(increment++);
            // System.out.println(increment.getAndIncrement());
        } catch (InterruptedException e) {}
    }
    
    public static void main(String... ss) {
        MyThread my = new MyThread();
        for (int i = 0; i < 10; i++) {
            new Thread(my).start();
        }
    }
}

※increment 有可能會有同時進入 CPU 的情形,使用 volatile 也沒有用

※java 1.5 新增了 java.util.concurrent.atomic 套件,專門做原子的操作

※incrementAndGet方法就是前置遞增了,而遞減單字是 decrement

※原子性採用 CAS (compare and swap) 算法,屬於樂觀鎖

※CAS 有三個值v和a都是主存抓來的,b是替換值,只有 v 和 a 的值相等才會將 b 的值取代 v 的值(CPU2 的圖錯了,v=1, a=0 才對)

※以上圖為例,CPU1 從主存抓到 i 的值是 0,此時 CPU2也進來抓到0
這時又換 CPU1,比較過後是一樣的,所以更新為 1,i 更新成功為1
這時又換 CPU2,因為 i 是 volatile,所以 v=1, a=0, 什麼也不做
※如果是 CAS 自旋,那 CPU2 還會繼續,這是 v 和 a 都是 1,就看 b 是什麼就可以取代

※判斷和塞新值要看成一個做法,也就是說不會在判斷時,另一個 Thread 進來的情況

※此例當然也是可以用 synchronized,但 synchronized 效能比較差

※還可看高手寫的文章

沒有留言:

張貼留言