動態

詳情 返回 返回

Java AtomicInteger類使用 - 動態 詳情

一個計數器

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

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();  
    }
}

多次執行新代碼,我們依舊發現結果不正確:

...
...
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的性能一下子會下降幾十倍。還有一個最大的問題就是多線程競爭一個鎖時,其餘未得到鎖的線程只能不停的嘗試獲得鎖,而不能中斷。這種情況下就會造成大量的競爭線程性能的下降。

Atomic

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

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();  
    }
}

Add a new 評論

Some HTML is okay.