Java併發——各種互斥技術的效率比較

    既然Java包括老式的synchronized關鍵字和Java SE5中心的Lock和Atomic類,那麼比較這些不一樣的方式,更多的理解他們各自的價值和適用範圍,就會顯得頗有意義。java

    比較天真的方式是在針對每種方式都執行一個簡單的測試,就像下面這樣:編程

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

abstract class Incrementable {
    protected long counter = 0;
    public abstract void increment();
}

class SynchronizingTest extends Incrementable {
    public synchronized void increment() { ++counter; }
}

class LockingTest extends Incrementable {
    private Lock lock = new ReentrantLock();
    public void increment() {
        lock.lock();
        try {
            ++counter;
        } finally {
            lock.unlock();
        }
    }
}

public class SimpleMicroBenchmark {
    static long test(Incrementable inc) {
        long start = System.nanoTime();
        for (long i = 0; i < 10000000; i++) {
            inc.increment();
        }
        return System.nanoTime() - start;
    }
    public static void main(String[] args) {
        long syncTime = test(new SynchronizingTest());
        long lockTime = test(new LockingTest());
        System.out.println(String.format("Synchronized: %1$10d", syncTime));
        System.out.println(String.format("Lock: %1$10d", lockTime));
        System.out.println(String.format(
            "Lock/Synchronized: %1$.3f", lockTime/(double)syncTime));
    }
}

執行結果(樣例):設計模式

Synchronized:  209403651
Lock:  257711686
Lock/Synchronized: 1.231

    從輸出中能夠看到,對synchronized方法的調用看起來要比使用ReentrantLock快,這是爲何呢?數組

    本例演示了所謂的「微基準測試」危險,這個屬於一般指在隔離的、脫離上下文環境的狀況下對某個個性進行性能測試。固然,你仍舊必須編寫測試來驗證諸如「Lock比synchronized更快」這樣的斷言,可是你須要在編寫這些測試的時候意識到,在編譯過程當中和在運行時實際會發生什麼。安全

    上面的示例存在着大量的問題。首先也是最重要的是,咱們只有在這些互斥存在競爭的狀況下,才能看到真正的性能差別,所以必須有多個任務嘗試訪問互斥代碼區。而在上面的示例中,每一個互斥都由單個的main()線程在隔離的狀況下測試的。服務器

    其次,當編譯器看到synchronized關鍵字時,有可能會執行特殊的優化,甚至有可能會注意到這個程序時單線程的。編譯器甚至可能會識別出counter被遞增的次數是固定數量的,所以會預先計算出其結果。不一樣的編譯器和運行時系統在這方面存在着差別,所以很難確切瞭解將會發生什麼,可是咱們須要防止編譯器去預測結果的可能性。併發

    爲了建立有效的測試,咱們必須把程序設計得更加複雜。首先,咱們須要多個任務,但並不僅是會修改內部值的任務,還包括讀取這些值的任務(不然優化器能夠識別出這些值歷來不會被使用)。另外,計算必須足夠複雜和不可預測,以使得編譯器沒有機會執行積極優化。這能夠經過預加載一個大型的隨機int數組(預加載能夠減少在主循環上調用Random.nextInt()所形成的影響),並在計算總和時使用它們來實現:dom

import java.util.Random;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

abstract class Accumulator {
    public static long cycles = 50000L;
    // Number of modifiers and readers during each test
    private static final int N = 4;
    public static ExecutorService exec = Executors.newFixedThreadPool(2 * N);
    private static CyclicBarrier barrier = new CyclicBarrier(2 * N + 1);
    protected volatile int index = 0;
    protected volatile long value = 0;
    protected long duration = 0;
    protected String id = "";
    // A big int array
    protected static final int SIZE = 100000;
    protected static int[] preLoad = new int[SIZE];
    static {
        // Load the array of random numbers:
        Random random = new Random(47);
        for (int i = 0; i < SIZE; i++) {
            preLoad[i] = random.nextInt();
        }
    }
    public abstract void accumulate();
    public abstract long read();
    private class Modifier implements Runnable {
        public void run() {
            for (int i = 0; i < cycles; i++) {
                accumulate();
            }
            try {
                barrier.await();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
    private class Reader implements Runnable {
        private volatile long value;
        public void run() {
            for (int i = 0; i < cycles; i++) {
                value = read();
            }
            try {
                barrier.await();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
    public void timedTest() {
        long start = System.nanoTime();
        for (int i = 0; i < N; i++) {
            exec.execute(new Modifier());//4 Modifiers
            exec.execute(new Reader());//4 Readers
        }
        try {
            barrier.await();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        duration = System.nanoTime() - start;
        System.out.println(String.format("%-13s: %13d", id, duration));
    }
    
    public static void report(Accumulator a1, Accumulator a2) {
        System.out.println(String.format("%-22s: %.2f", a1.id + 
            "/" + a2.id, a1.duration / (double)a2.duration));
    }
}

class BaseLine extends Accumulator {
    {id = "BaseLine";}
    public void accumulate() {
        value += preLoad[index++];
        if (index >= SIZE - 5) index = 0;
    }

    public long read() { return value; }
}

class SynchronizedTest extends Accumulator {
    {id = "Synchronized";}
    public synchronized void accumulate() {
        value += preLoad[index++];
        if (index >= SIZE - 5) index = 0;
    }
    
    public synchronized long read() { return value; }
}

class LockTest extends Accumulator {
    {id = "Lock";}
    private Lock lock = new ReentrantLock();
    public void accumulate() {
        lock.lock();
        try {
            value += preLoad[index++];
            if (index >= SIZE - 5) index = 0;
        } finally {
            lock.unlock();
        }
    }
    
    public long read() { 
        lock.lock();
        try {
            return value; 
        } finally {
            lock.unlock();
        }
    }
}

class AtomicTest extends Accumulator {
    {id = "Atomic"; }
    private AtomicInteger index = new AtomicInteger(0);
    private AtomicLong value = new AtomicLong(0);
    public void accumulate() {
        //Get value before increment.
        int i = index.getAndIncrement();
        //Get value before add.
        value.getAndAdd(preLoad[i]);
        if (++i >= SIZE - 5) index.set(0);
    }

    public long read() {return value.get(); }
}

public class SynchronizationComparisons {
    static BaseLine baseLine = new BaseLine();
    static SynchronizedTest synchronizedTest = new SynchronizedTest();
    static LockTest lockTest = new LockTest();
    static AtomicTest atomicTest = new AtomicTest();
    static void test() {
        System.out.println("============================");
        System.out.println(String.format(
            "%-13s:%14d", "Cycles", Accumulator.cycles));
        baseLine.timedTest();
        synchronizedTest.timedTest();
        lockTest.timedTest();
        atomicTest.timedTest();
        Accumulator.report(synchronizedTest, baseLine);
        Accumulator.report(lockTest, baseLine);
        Accumulator.report(atomicTest, baseLine);
        Accumulator.report(synchronizedTest, lockTest);
        Accumulator.report(synchronizedTest, atomicTest);
        Accumulator.report(lockTest, atomicTest);
    }
    public static void main(String[] args) {
        int iterations = 5;//Default execute time
        if (args.length > 0) {//Optionally change iterations
            iterations = Integer.parseInt(args[0]);
        }
        //The first time fills the thread pool
        System.out.println("Warmup");
        baseLine.timedTest();
        //Now the initial test does not include the cost
        //of starting the threads for the first time.
        for (int i = 0; i < iterations; i++) {
            test();
            //Double cycle times.
            Accumulator.cycles *= 2;
        }
        Accumulator.exec.shutdown();
    }
}

執行結果(樣例):性能

Warmup
BaseLine     :      12138900
============================
Cycles       :         50000
BaseLine     :      12864498
Synchronized :      87454199
Lock         :      27814348
Atomic       :      14859345
Synchronized/BaseLine : 6.80
Lock/BaseLine         : 2.16
Atomic/BaseLine       : 1.16
Synchronized/Lock     : 3.14
Synchronized/Atomic   : 5.89
Lock/Atomic           : 1.87
============================
Cycles       :        100000
BaseLine     :      25348624
Synchronized :     173022095
Lock         :      51439951
Atomic       :      32804577
Synchronized/BaseLine : 6.83
Lock/BaseLine         : 2.03
Atomic/BaseLine       : 1.29
Synchronized/Lock     : 3.36
Synchronized/Atomic   : 5.27
Lock/Atomic           : 1.57
============================
Cycles       :        200000
BaseLine     :      47772466
Synchronized :     348437447
Lock         :     104095347
Atomic       :      59283429
Synchronized/BaseLine : 7.29
Lock/BaseLine         : 2.18
Atomic/BaseLine       : 1.24
Synchronized/Lock     : 3.35
Synchronized/Atomic   : 5.88
Lock/Atomic           : 1.76
============================
Cycles       :        400000
BaseLine     :      98804055
Synchronized :     667298338
Lock         :     212294221
Atomic       :     137635474
Synchronized/BaseLine : 6.75
Lock/BaseLine         : 2.15
Atomic/BaseLine       : 1.39
Synchronized/Lock     : 3.14
Synchronized/Atomic   : 4.85
Lock/Atomic           : 1.54
============================
Cycles       :        800000
BaseLine     :     178514302
Synchronized :    1381579165
Lock         :     444506440
Atomic       :     300079340
Synchronized/BaseLine : 7.74
Lock/BaseLine         : 2.49
Atomic/BaseLine       : 1.68
Synchronized/Lock     : 3.11
Synchronized/Atomic   : 4.60
Lock/Atomic           : 1.48

    這個程序使用了模板方法設計模式,將全部的共用代碼都放置到基類中,並將全部不一樣的代碼隔離在子類的accumulate()和read()的實現中。在每一個子類SynchronizedTest、LockTest和AtomicTest中,你能夠看到accumulate()和read()如何表達了實現互斥現象的不一樣方式。測試

    在這個程序中,各個任務都是經由FixedThreadPool執行的,在執行過程當中嘗試着在開始時跟蹤全部線程的建立,而且在測試過程當中方式產生任何額外的開銷。爲了保險起見,初始測試執行了兩次,而第一次的結果被丟棄,由於它包含了初試線程的建立。

    程序中有一個CyclicBarrier,由於咱們但願確保全部的任務在聲明每一個測試完成以前都已經完成。

    每次調用accumulate()時,它都會移動到preLoad數組的下一個位置(到達數組尾部時在回到開始位置),並將這個位置的隨機生成的數字加到value上。多個Modifier和Reader任務提供了在Accumulator對象上的競爭。

    注意,在AtomicTest中,我發現狀況過於複雜,使用Atomic對象已經不適合了——基本上,若是涉及多個Atomic對象,你就有可能會被強制要求放棄這種用法,轉而使用更加常規的互斥(JDK文檔特別聲明:當一個對象的臨界更新被限制爲只涉及單個變量時,只有使用Atomic對象這種方式才能工做)。可是,這個測試仍舊保留了下來,使你可以感覺到Atomic對象的性能優點。

    在main()中,測試時重複運行的,而且你能夠要求其重複的次數超過5次,對於每次重複,測試循環的數量都會加倍,所以你能夠看到當運行次數愈來愈多時,這些不一樣的互斥在行爲方面存在着怎樣的差別。正如你從輸出中看到的那樣,測試結果至關驚人。拋開預加載數組、初始化線程池和線程的影響,synchronized關鍵字的效率明顯比Lock和Atomic的低。

    記住,這個程序只是給出了各類互斥方式之間的差別的趨勢,而上面的輸出也僅僅表示這些差別在個人特定環境下的特定機器上的表現。如你所見,若是本身動手實驗,當全部的線程數量不一樣,或者程序運行的時間更長時,在行爲方面確定會存在着明顯的變化。例如,某些hotspot運行時優化會在程序運行後的數分鐘以後被調用,可是對於服務器端程序,這段時間可能長達數小時。

    也就是說,很明顯,使用Lock一般會比使用synchronized高效許多,並且synchronized的開銷看起來變化範圍太大,而Lock則相對一致

    這是否意味着你永遠不該該選擇synchronized關鍵字呢?這裏有兩個因素須要考慮:首先,在上面的程序中,互斥方法體是很是小的。一般,這是一個好的習慣——只互斥那些你絕對必須互斥的部分。可是,在實際中,被互斥部分可能會比上面示例中的那些大許多,所以在這些方法體中花費的時間的百分比可能會明顯大於進入和退出互斥的開銷,這樣也就湮沒了提升互斥速度帶來的全部好處。固然,惟一瞭解這一點的方式是——當你在對性能調優時,應該當即——嘗試各類不一樣的方法並觀察它們形成的影響。

    其次,在閱讀本文的代碼你就會發現,很明顯,synchronized關鍵字所產生的代碼,與Lock所須要的「加鎖-try/finally-解鎖」慣用法所產生的代碼量相比,可讀性提升了不少。在編程時,與其餘人交流對於與計算機交流而言要重要得多,所以代碼的可讀性相當重要。所以,在編程時,以synchronized關鍵字入手,只有在性能調優時才替換爲Lock對象這種作法,是具備實際意義的。

    最後,當你在本身的併發程序中可使用Atomic類時,這確定很是好,可是要意識到,正如咱們在上例中看到的,Atomic對象只有在很是簡單的狀況下才有用,這些狀況一般包括你只有一個要被修改的Atomic對象,而且這個對象獨立於其餘全部的對象。更安全的作法是:以更加傳統的方式入手,只有在性能方面的需求可以明確指示時,才替換爲Atomic。

相關文章
相關標籤/搜索