1. 程式人生 > >Java 多線程 volitile 和 atomic

Java 多線程 volitile 和 atomic

以及 expec mask 理解 native start 解決辦法 次循環 cpu

Java 多線程 volitile 和 atomic

volitile關鍵字

public class MTester {
    public static class TestKey{
        int x = 0;
    }
    public static TestKey key0 = new TestKey();
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            while (key0.x == 0){
            }
            System.out.println("key0"+key0.x);
        });
        Thread thread1 = new Thread(()->{
            try {
                Thread.sleep(1000);
                key0.x=1;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                System.out.println("over");
            }
        });
        thread.start();
        thread1.start();
    }
}

嘗試運行以上代碼,發現thread永遠也無法發現key0的x被改變

技術分享圖片

所以這個時候需要加上volitile關鍵字

技術分享圖片

具體原因是java中每個線程都有工作內存,以及主存

我的理解就是不加volitile,線程讀寫變量是先在自己的工作內存中處理,然後再寫回主存,但是有的線程處理的是工作內存,但是並沒有從主存裏面讀取,加上volitile關鍵字之後,會通知其他線程,讓他們強制從主存中讀取數據

https://www.cnblogs.com/zhengbin/p/5654805.html

volatile還有一個特性:禁止指令重排序優化。

可以見這個文章

https://www.cnblogs.com/chengxiao/p/6528109.html

但是volitile只能保證可見性,不能保證原子性,也就是說如果多線程操作 i++還是無法保證正確

ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0;i<10;i++){
    service.submit(()->{
        for (int j = 0;j <10;j++){
            key0.x++;
            System.out.println(key0.x);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}
service.awaitTermination(10, TimeUnit.SECONDS);
System.out.println(key0.x);

技術分享圖片

還是無法保證原子性

這個時候可以考慮使用 java.util.concurrent.atomic;中的類

這些類裏面的類大部分都是使用CAS算法進行操作的

CAS compare and set

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

這個 unsafe是unsafe類,裏面的方法都是native方法

CAS其實就是期望的值進行比較,如果不相等,就證明有其他線程更改過了,然後不執行操作然後返回失敗,CAS看起來很麻煩,但是卻可以映射一些CPU指令,實際上執行起來還是很快的(參考java核心技術)

unsafe裏面的方法大部分都是native方法

技術分享圖片

比如說我們想要對AtomicInteger執行一個 increase操作,就先比較自己跟期望的值,如果不等,那就在下次循環接著嘗試更改,直到更改成功

//AtomicInteger.java
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
//Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

不過這種不斷嘗試比較,對CPU開銷還是比較大,不過相對於synchronized來說更輕量級,因為synchronized需要不斷嘗試獲取鎖釋放鎖,而且只能獨占

在並發量不是特別大的情況下,效率相對於synchronized還是很高的,當自選嚴重沖突的時候synchronized還是效率更高一些

CAS 算法屬於自旋

不過CAS算法也有其他的缺點,常見的就是ABA問題

舉個例子

線程1:從內存位置 1 取回A

線程2:從內存位置 1取到 A

線程2:做了一些操作

線程2:從內存位置 1 寫入A

線程1:發現位置1還是A CAS成功但是卻不知道線程2做了什麽操作,可能引發一些後果

解決辦法

AtomicStampedReference

可以用一個timestamp 或者mask來判斷是否有其他操作

自旋鎖的簡單實現:

思路:每次只有一個線程進入臨界區

import java.util.concurrent.atomic.AtomicReference;

public class MSpinLock {
    AtomicReference<Thread> reference = new AtomicReference<>();
    public void lock(){
        do {

        }while (!this.reference.compareAndSet(null,Thread.currentThread()));
    }
    public void unlock(){
        do {

        }while (!this.reference.compareAndSet(Thread.currentThread(),null));
    }

    public static void main(String[] args) {
        MSpinLock lock = new MSpinLock();
        Thread thread = new Thread(()->{
            while (true){
                try{
                    lock.lock();
                    System.out.println("thread had lock");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("thread will unlock");
                    lock.unlock();
                }
            }
        });
        Thread thread1 = new Thread(()->{
            while (true){
                try{
                    lock.lock();
                    System.out.println("thread1 had lock");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("thread1 will unlock");
                    lock.unlock();
                }
            }

        });

        thread.start();
        thread1.start();
    }
}

技術分享圖片

每次都是成對出現的

如果註釋掉lock

技術分享圖片

顯然不對

不過我們做的自選鎖不可重入

假如有個函數需要遞歸,那麽自旋鎖就會發生死鎖

所以我們需要一個Integer來判斷一下

    public void lock(){
        if(Thread.currentThread().equals(reference.get())){
            atomicInteger.incrementAndGet();
            return ;
        }
        do {
        }while (!this.reference.compareAndSet(null,Thread.currentThread()));
        atomicInteger.incrementAndGet();
    }
    public void unlock(){
        if(Thread.currentThread().equals(reference.get())){
            int n = atomicInteger.decrementAndGet();
            if(n>0){
                return;
            }
        }
        do {
        }while (!this.reference.compareAndSet(Thread.currentThread(),null));

    }

這樣就可重入了

https://www.cnblogs.com/qjjazry/p/6581568.html

Java 多線程 volitile 和 atomic