Java并发——各类互斥技术的效率比较

Wesley13
• 阅读 728

    既然Java包括老式的synchronized关键字和Java SE5中心的Lock和Atomic类,那么比较这些不同的方式,更多的理解他们各自的价值和适用范围,就会显得很有意义。

    比较天真的方式是在针对每种方式都执行一个简单的测试,就像下面这样:

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()所造成的影响),并在计算总和时使用它们来实现:

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。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Java服务总在半夜挂,背后的真相竟然是... | 京东云技术团队
最近有用户反馈测试环境Java服务总在凌晨00:00左右挂掉,用户反馈Java服务没有定时任务,也没有流量突增的情况,Jvm配置也合理,莫名其妙就挂了
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这