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

既然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。

时间: 2024-08-11 12:39:56

Java并发——各类互斥技术的效率比较的相关文章

Java并发编程与技术内幕:聊聊锁的技术内幕(上)

林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 一.基础知识 在Java并发编程里头,锁是一个非常重要的概念.就如同现实生活一样,如果房子上了锁.别人就进不去.Java里头如果一段代码取得了一个锁,其它地方再想去这个锁(或者再执行这个相同的代码)就都得等待锁释放.锁其实分成非常多.比如有互斥锁.读写锁.乐观锁.悲观锁.自旋锁.公平锁.非公平锁等.包括信号量其实都可以认为是一个锁. 1.什么时需要锁呢? 其实非常多的场景,如共享实例变量.共

Java并发编程与技术内幕:线程池深入理解

林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 摘要: 本文主要讲了Java当中的线程池的使用方法.注意事项及其实现源码实现原理,并辅以实例加以说明,对加深Java线程池的理解有很大的帮助. 首先,讲讲什么是线程池?照笔者的简单理解,其实就是一组线程实时处理休眠状态,等待唤醒执行.那么为什么要有线程池这个东西呢?可以从以下几个方面来考虑:其一.减少在创建和销毁线程上所花的时间以及系统资源的开销 .其二.2将当前任务与主线程隔离,能实现和主

Java并发编程与技术内幕:ArrayBlockingQueue、LinkedBlockingQueue及SynchronousQueue源码解析

林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 摘要:本文主要讲了Java中BlockingQueue的源码 一.BlockingQueue介绍与常用方法 BlockingQueue是一个阻塞队列.在高并发场景是用得非常多的,在线程池中.如果运行线程数目大于核心线程数目时,也会尝试把新加入的线程放到一个BlockingQueue中去.队列的特性就是先进先出很容易理解,在java里头它的实现类主要有下图的几种,其中最常用到的是ArrayBl

Java并发编程与技术内幕:CopyOnWriteArrayList、CopyOnWriteArraySet源码解析

林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 摘要:本文主要讲了Java中CopyOnWriteArrayList .CopyOnWriteArraySet的源码分析 一.CopyOnWriteArrayList源码分析 CopyOnWriteArrayList在java的并发场景中用得其实并不是非常多,因为它并不能完全保证读取数据的正确性.其主要有以下的一些特点:1.适合场景读多写少2.不能保证读取数据一定是正确 的,因为get时是不

Java并发编程与技术内幕:Callable、Future、FutureTask、CompletionService

林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 在上一文章中,笔者介绍了线程池及其内部的原理.今天主要讲的也是和线程相关的内容.一般情况下,使用Runnable接口.Thread实现的线程我们都是无法返回结果的.但是如果对一些场合需要线程返回的结果.就要使用用Callable.Future.FutureTask.CompletionService这几个类.Callable只能在ExecutorService的线程池中跑,但有返回结果,也可

Java并发编程与技术内幕:聊聊锁的技术内幕(中)

摘要:本文主要讲了读写锁. 一.读写锁ReadWriteLock 在上文中回顾了并发包中的可重入锁ReentrantLock,并且也分析了它的源码.从中我们知道它是一个单一锁(笔者自创概念),意思是在多人读.多人写.或同时有人读和写时.只能有一个人能拿到锁,执行代码.但是在很多场景.我们想控制它能多人同时读,但是又不让它多人写或同时读和写时.(想想这是不是和数据库的可重复读有点类型?),这时就可以使用读写锁:ReadWriteLock. 下面来看一个应用 package com.lin; imp

【java并发】线程技术之死锁问题

我们知道,使用synchronized关键字可以有效的解决线程同步问题,但是如果不恰当的使用synchronized关键字的话也会出问题,即我们所说的死锁.死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放.由于线程被无限期地阻塞,因此程序不可能正常终止. 下面写一个死锁的例子加深理解.先看程序,再来分析一下死锁产生的原因: public class DeadLock { public static void main(String[] args) { Busin

[Todo] Java并发编程学习

有两个系列的博文,交替着可以看看: 1. Java并发编程与技术内幕 http://blog.csdn.net/Evankaka/article/details/51866242 2. [Java并发编程]并发编程大合集 http://blog.csdn.net/ns_code/article/details/17539599

【Java】线程并发、互斥与同步

网络上对于线程的解析总是天花龙凤的,给你灌输一大堆概念,考研.本科的操作系统必修课尤甚,让你就算仔细看完一大堆文章都不知道干什么. 下面不取网站复制粘贴,在讲解自己的Java线程并发.互斥与同步之前先给大家解构操作系统书中那些给出书者为了出书者而写的废话到底是什么意思. 大神们如果只想看程序,可以自行跳过,反正我的文章从来新手向,不喜勿看. 一.线程的基本概念 其实线程的概念非常简单,多一个线程,就多了一个做事情的人. 比如搬东西,搬10箱货物,这就是所谓的一个进程,一个人,就是所谓的一个线程,