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。

相关文章
相关标签/搜索