Java AtomicInteger類使用

一個計數器

對於普通的變量,在涉及多線程操做時,會遇到經典的線程安全問題。考慮以下代碼:java

private static final int TEST_THREAD_COUNT = 100;
private static int counter = 0;

public static void main(String[] args) {
    final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);  
    Thread[] threads = new Thread[TEST_THREAD_COUNT];

    for (int i = 0; i < TEST_THREAD_COUNT; i++) {
        threads[i] = new Thread(new Runnable() {

            @Override
            public  void run() {
                ++counter;
                System.out.println("Thread " + Thread.currentThread().getId() + "  / Counter : " + counter);
                latch.countDown();
            }
        });
        threads[i].start();  

    }

    try {
        latch.await();
        System.out.println("Main Thread " + "  / Counter : " + counter);
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }
}

屢次執行這段程序,咱們會發現最後counter的值會出現98,99等值,而不是預想中的100程序員

...
...
Thread 100  / Counter : 90
Thread 101  / Counter : 91
Thread 102  / Counter : 92
Thread 103  / Counter : 93
Thread 104  / Counter : 95
Thread 105  / Counter : 95
Thread 106  / Counter : 96
Thread 107  / Counter : 97
Thread 108  / Counter : 98
Thread 109  / Counter : 99
Main Thread   / Counter : 99

這個問題發生的緣由是++counter不是一個原子性操做。當要對一個變量進行計算的時候,CPU須要先從內存中將該變量的值讀取到高速緩存中,再去計算,計算完畢後再將變量同步到主內存中。這在多線程環境中就會遇到問題,試想一下,線程A從主內存中複製了一個變量a=3到工做內存,而且對變量a進行了加一操做,a變成了4,此時線程B也從主內存中複製該變量到它本身的工做內存,它獲得的a的值仍是3,a的值不一致了(這裏工做內存就是高速緩存)。緩存

2019-10-21 15-07-56屏幕截圖.png

同步

java有個sychronized關鍵字,它能後保證同一個時刻只有一條線程可以執行被關鍵字修飾的代碼,其餘線程就會在隊列中進行等待,等待這條線程執行完畢後,下一條線程才能對執行這段代碼。
它的修飾對象有如下幾種:安全

  1. 修飾一個代碼塊,被修飾的代碼塊稱爲同步語句塊,其做用的範圍是大括號{}括起來的代碼,做用的對象是調用這個代碼塊的對象;
  2. 修飾一個方法,被修飾的方法稱爲同步方法,其做用的範圍是整個方法,做用的對象是調用這個方法的對象;
  3. 修飾一個靜態的方法,其做用的範圍是整個靜態方法,做用的對象是這個類的全部對象;
  4. 修飾一個類,其做用的範圍是synchronized後面括號括起來的部分,做用主的對象是這個類的全部對象。

如今咱們開始使用咱們的新知識,調整以上代碼,在run()上添加sychronized關鍵字。多線程

private static final int TEST_THREAD_COUNT = 100;
private static int counter = 0;

public static void main(String[] args) {
    final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);  
    Thread[] threads = new Thread[TEST_THREAD_COUNT];

    for (int i = 0; i < TEST_THREAD_COUNT; i++) {
        threads[i] = new Thread(new Runnable() {

            @Override
            public synchronized void run() {
                ++counter;
                System.out.println("Thread " + Thread.currentThread().getId() + "  / Counter : " + counter);
                latch.countDown();
            }
        });
        threads[i].start();  

    }

    try {
        latch.await();
        System.out.println("Main Thread " + "  / Counter : " + counter);
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }
}

屢次執行新代碼,咱們依舊發現結果不正確:ide

...
...
Thread 98  / Counter : 87
Thread 97  / Counter : 86
Thread 99  / Counter : 89
Thread 100  / Counter : 89
Thread 101  / Counter : 90
Thread 102  / Counter : 91
Thread 104  / Counter : 95
Thread 108  / Counter : 97
Thread 106  / Counter : 96
Thread 105  / Counter : 95
Thread 103  / Counter : 95
Thread 109  / Counter : 98
Thread 107  / Counter : 97
Main Thread   / Counter : 98

這裏的緣由在於synchronized是鎖定當前實例對象的代碼塊。也就是當多條線程操做同一個實例對象的同步方法是時,只有一條線程能夠訪問,其餘線程都須要等待。這裏Runnable實例有多個,因此鎖就不起做用。
咱們繼續修改代碼,使得Runnable實例只有一個:性能

private static final int TEST_THREAD_COUNT = 100;
private static int counter = 0;
private final static CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);

static class MyRunnable implements Runnable {

    @Override
    public synchronized void run() {
        ++counter;
        System.out.println("Thread " + Thread.currentThread().getId() + "  / Counter : " + counter);
        latch.countDown();
    }
    
}

public static void main(String[] args) {
    Thread[] threads = new Thread[TEST_THREAD_COUNT];

    MyRunnable myRun = new MyRunnable();
    for (int i = 0; i < TEST_THREAD_COUNT; i++) {
        threads[i] = new Thread(myRun);
        threads[i].start();
    }

    try {
        latch.await();
        System.out.println("Main Thread " + "  / Counter : " + counter);
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }
}

如今咱們發現屢次執行代碼後,最後結果都是100
咱們能夠給counter變量添加volatile關鍵字(這裏它對於結果沒有影響)。
當一個變量被定義爲volatile以後,它對全部的線程就具備了可見性,也就是說當一個線程修改了該變量的值,全部的其它線程均可以當即知道。經過synchronizedLock也可以保證可見性,synchronizedLock能保證同一時刻只有一個線程獲取鎖而後執行同步代碼,而且在釋放鎖以前會將對變量的修改刷新到主存當中。所以能夠保證可見性。優化

sychronized的優缺點

synchronized在發生異常時,會自動釋放線程佔有的鎖,所以不會致使死鎖現象發生。另外在資源競爭不是很激烈的狀況下,偶爾會有同步的情形下,synchronized是很合適的。緣由在於,編譯程序一般會盡量的進行優化synchronized,另外可讀性很是好,無論用沒用過5.0多線程包的程序員都能理解。可是當同步競爭很是激烈的時候,synchronized的性能一會兒會降低幾十倍。還有一個最大的問題就是多線程競爭一個鎖時,其他未獲得鎖的線程只能不停的嘗試得到鎖,而不能中斷。這種狀況下就會形成大量的競爭線程性能的降低。atom

Atomic

針對synchronized的一系列缺點,JDK5提供了Lock類,目的是爲同步機制進行改善。Locksynchronized有一點很是大的不一樣,採用synchronized不須要用戶手動的去釋放鎖,當synchronized方法或者代碼塊執行完畢以後,系統會自動的讓線程釋放對鎖的佔有,而Lock則必需要用戶去手動釋放鎖,若是沒有主動的釋放鎖,就會可能致使出現死鎖的現象。不過這篇文章這裏不討論Lock類。
在Java 1.5的java.util.concurrent.atomic包下提供了一些原子操做類,即對基本數據類型的 自增(加1操做),自減(減1操做)、以及加法操做(加一個數),減法操做(減一個數)進行了封裝,保證這些操做是原子性操做
咱們這裏使用AtomicIntegerspa

private static final int TEST_THREAD_COUNT = 100;
private static AtomicInteger at = new AtomicInteger(0);

public static void main(String[] args) {
    final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);  
    Thread[] threads = new Thread[TEST_THREAD_COUNT];

    for (int i = 0; i < TEST_THREAD_COUNT; i++) {
        threads[i] = new Thread(new Runnable() {

            @Override
            public void run() {
                int value = at.incrementAndGet();
                System.out.println("Thread " + Thread.currentThread().getId() + "  / Counter : " + value);
                latch.countDown();
            }
        });
        threads[i].start();
    }
    
    try {
        latch.await();
        System.out.println("Main Thread " + "  / Counter : " + at.get());
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }
}
相關文章
相關標籤/搜索