※volatile
尤於 CPU 的速度比主存還快,所以每個 Thread 都會有一個專屬的 cache,將主存的資料從主存拷過去
volatile 能保證 1.內存可見性和 2.排序性
.volatile 只能宣告在全域變數
.內存可見性:volatile 有揮發性的意思,意思就是用完就丟,在 java 裡的意思是值有改變就去主存抓
以上面的圖來說,要是值改變了,就相當於沒有 CPU cache 了,所以會到主存去抓 (保下面的賣票例子不可用 volatile,因為沒有原子性)
.排序性:CPU 會將我們寫的程式碼重排,如寫在第一行,但不一定就是先執行,不過他保證結果是一樣的,但可惜只是單線程一樣,多線程有可能會不一樣,而加上這個關鍵字可以禁止 CPU 重排
.只能解決一寫多讀的情形
.++i 是原子性;i++ 不是原子性,可用 AtomicXXX 或 LongAdder 產生原子性,高併發時用 LongAdder 較快,否則 AtomicXXX 較快
※
※此例如果不加 volatile 有可能兩個 CPU 同時抓到,所以迴圈裡的 flag 永遠都是 false了
※此例也可用 synchronized 或 Lock,但效能比較差
※不代表 volatile 可取代 synchronized
volatile 並沒有互斥性,也不能保證原子性
互斥性:像 synchronized 和 Lock 就有,一次只能一個進入,但指的是相同的物件
例:HttpSession 可以鎖定成功;但 HttpServletRequest 就不可能鎖定成功
※還沒加原子性:雖然在減 1 時,其他線程有看見,但有可能還沒寫,其他線程就讀到舊的,因為 volatile 沒有原子性
※加原子性的 AtomicInteger 後,使用兩個線程看起來好像可以,實際上還是有問題,因為此例是偶數票,每次少兩張,最後是 0,但用更多的線程去跑或改成單數票又不行了
.因為雖然加了原子性,但在改之前,其他線程還是讀的到 (資料庫的不可重覆讀)
如第 1 次,四個線程都讀到 10,然後都減 1,但有原子性,所以最後是 6
而第 2 次,四個線程都讀到 6,然後都減 1,還是有原子性,所以最後是 2
而第 3 次,四個線程都讀到 2,然後都減 1,還是有原子性,所以最後是 -2
也就是說一次只能有一個線程讀取,所以必需要有互斥性
※
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 無法保證這一點
※
※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 效能比較差
※還可看高手寫的文章
※原子性
表示不能切割,如後置遞增/減例: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 效能比較差
※還可看高手寫的文章
沒有留言:
張貼留言