天天看點

沒用過Java原子類?我來手寫一個AtomicInteger往期推薦

問題引出

大家可能聽過「Automic」原子類,這些類在多線程下可以保證線程安全。比如想要實作自增,多線程下會出現少增加的情況。

public class VolatileAtomicTest {
    public static int num = 0;

    public  static void increase() {
        num++;
    }  

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increase();
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println(num);
    }
}
           

上面代碼建立了10個線程,這10個線程要依次循環1000次,每次增加1,最後要求。

但實際情況是

沒用過Java原子類?我來手寫一個AtomicInteger往期推薦

多線程情況下會出現第一個線程還在運算,第二個線程就運算完并覆寫了第一個線程的值運算結果,是以會出現與預期不符的結果。原因在

public  static void increase() {
        num++;
} 
           

num++

我們可以拆解為

int a = num + 1;   //步驟1
num = a;           //步驟2
           

如果線程一隻執行到步驟1,還沒執行到步驟2,線程二這時執行了步驟2。那麼num的值就是線程2計算的值,而線程一的值就覆寫了。

如果我們保證這兩步的原子性(操作一體,不能被其他線程插入)就可以得到預期結果。我們在這個方法下面加鎖即可。

public synchronized static void increase() {
    num++;
}
           

或者

static Lock lock = new ReentrantLock();

public static void increase() {
    try {
        lock.lock();
        num++;
    } finally {
        lock.unlock();
    }
} 
           
沒用過Java原子類?我來手寫一個AtomicInteger往期推薦

但是,上面的方法都使用了鎖,在多線程下對性能還是有影響的。我們可以使用無鎖化的原子類,實作原子自增。

@Test
public void test() throws InterruptedException {
    Thread[] threads = new Thread[10];
    for (int i = 0; i < threads.length; i++) {
        threads[i] = new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
                num = atomicInteger.incrementAndGet();
            }
        });
        threads[i].start();
    }
    for (Thread thread : threads) {
        thread.join();
    }
    System.out.println(num);
}
           
沒用過Java原子類?我來手寫一個AtomicInteger往期推薦

原子類

沒用過Java原子類?我來手寫一個AtomicInteger往期推薦

上圖就是Java原子類的全家桶,主要是通過CAS + 自旋實作的。這裡我們主要說說

AtomicInteger

,來看看

incrementAndGet()

方法。Java8增加了一些類,優化自選帶來的性能問題。

/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
           
/ setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
           

發現用了

Unsafe

getAndAddInt

方法。至于

Unsafe

,底層也是作業系統的類,可以直接修改作業系統記憶體,或者排程線程。看着名字就知道不建議程式員使用它。

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;
}
           
  • var1

    Object

  • var2

    valueOffset

  • var5

    : 目前該變量在記憶體中的值
  • var5 + var4

    : 需要寫進去的值

這裡就是通過CAS修改值,不斷循環,知道修改成功。但在高并發情況下,存在一些問題:

高并發量的情況下,由于真正更新成功的線程占少數,容易導緻循環次數過多,浪費時間,并且浪費線程資源。

由于需要保證變量真正的共享,「緩存一緻性」開銷變大。

getIntVolatile()

方法是系統的本地方法

public native int getIntVolatile(Object var1, long var2);
           

compareAndSwapInt()

也是本地方法,這裡就是常說的CAS了。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
           

手寫

AtomicInteger

「首先定義幾個變量」

private volatile int value;

private static long offset;//偏移位址

private static Unsafe unsafe;
           

我們定義了

value

值,用來儲存目前的值。offset偏移位址,在類初始化的時候,計算出value變量在對象中的偏移。

Unsafe

類,直接作業系統記憶體。

「初始化變量」

// 通過Unsafe計算出value變量在對象中的偏移
static {
    try {
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafeField.setAccessible(true);
        unsafe = (Unsafe) theUnsafeField.get(null);
        Field field = MyAtomicInteger.class.getDeclaredField("value");
        offset = unsafe.objectFieldOffset(field);//獲得偏移位址
    } catch (Exception e) {
        e.printStackTrace();
    }
}
           

我們反射擷取

Unsafe

theUnsafe

字段,自定義的

value

字段,還有偏移位址

offset

「自增方法」

public void increment(int num) {
    int tempValue;
    do {
        tempValue = unsafe.getIntVolatile(this, offset);//拿到值
    } while (!unsafe.compareAndSwapInt(this, offset, tempValue, value + num));//CAS自旋
}
           

「測試結果」

public class MyAtomTest {
    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        MyAtomicInteger atomicInteger = new MyAtomicInteger();
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicInteger.increment(1);  //自增1
                }
            });
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("x=" + atomicInteger.get());
    }
}
           
沒用過Java原子類?我來手寫一個AtomicInteger往期推薦

「Atomic」原子類解決了高并發下線程安全問題,但是高并發下也帶來了性能問題,如果你的項目需要使用原子類,并且性能要求高,可以使用「Java8」中的原子類。

往期推薦

  • 我寫出這樣幹淨的代碼,老闆直誇我
  • 雲南麗江旅遊攻略
  • 使用ThreadLocal怕記憶體洩漏?
  • Java進階之路思維導圖
  • 程式員必看書籍推薦
  • 3萬字的Java後端面試總結(附PDF)

掃碼二維碼,擷取更多精彩。或微信搜Lvshen_9,可背景回複擷取資料

1.回複"java" 擷取java電子書;


2.回複"python"擷取python電子書;


3.回複"算法"擷取算法電子書;


4.回複"大資料"擷取大資料電子書;


5.回複"spring"擷取SpringBoot的學習視訊。


6.回複"面試"擷取一線大廠面試資料


7.回複"進階之路"擷取Java進階之路的思維導圖


8.回複"手冊"擷取阿裡巴巴Java開發手冊(嵩山終極版)


9.回複"總結"擷取Java後端面試經驗總結PDF版


10.回複"Redis"擷取Redis指令手冊,和Redis專項面試習題(PDF)
           

另:點選【我的福利】有更多驚喜哦。

沒用過Java原子類?我來手寫一個AtomicInteger往期推薦