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

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

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

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

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

    }

}

执行结果(样例):

?


1

2

3

Synchronized:  209403651

Lock:  257711686

Lock/Synchronized: 1.231

    从输出中可以看到,对synchronized方法的调用看起来要比使用ReentrantLock快,这是为什么呢?

    本例演示了所谓的“微基准测试”危险,这个属于通常指在隔离的、脱离上下文环境的情况下对某个个性进行性能测试。当然,你仍旧必须编写测试来验证诸如“Lock比synchronized更快”这样的断言,但是你需要在编写这些测试的时候意识到,在编译过程中和在运行时实际会发生什么。

    上面的示例存在着大量的问题。首先也是最重要的是,我们只有在这些互斥存在竞争的情况下,才能看到真正的性能差异,因此必须有多个任务尝试访问互斥代码区。而在上面的示例中,每个互斥都由单个的main()线程在隔离的情况下测试的。

    其次,当编译器看到synchronized关键字时,有可能会执行特殊的优化,甚至有可能会注意到这个程序时单线程的。编译器甚至可能会识别出counter被递增的次数是固定数量的,因此会预先计算出其结果。不同的编译器和运行时系统在这方面存在着差异,因此很难确切了解将会发生什么,但是我们需要防止编译器去预测结果的可能性。

    为了创建有效的测试,我们必须是程序更加复杂。首先,我们需要多个任务,但并不只是会修改内部值的任务,还包括读取这些值的任务(否则优化器可以识别出这些值从来不会被使用)。另外,计算必须足够复杂和不可预测,以使得编译器没有机会执行积极优化。这可以通过预加载一个大型的随机int数组(预加载可以减小在主循环上调用Random.nextInt()所造成的影响),并在计算总和时使用它们来实现:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

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

    }

}

执行结果(样例):

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

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-09-12 16:54:23

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

【JAVA秒会技术之多线程】Java 并发工具包 java.util.concurrent 用户指南

1. java.util.concurrent - Java 并发工具包 Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包.这个包包含有一系列能够让 Java 的并发编程变得更加简单轻松的类.在这个包被添加以前,你需要自己去动手实现自己的相关工具类.本文我将带你一一认识 java.util.concurrent 包里的这些类,然后你可以尝试着如何在项目中使用它们.本文中我将使用 Java 6 版本,我不确定这和 Java 5 版本里的是否有一些差异.

Java并发集合的实现原理

本文简要介绍Java并发编程方面常用的类和集合,并介绍下其实现原理. AtomicInteger 可以用原子方式更新int值.类 AtomicBoolean.AtomicInteger.AtomicLong 和 AtomicReference 的实例各自提供对相应类型单个变量的访问和更新.基本的原理都是使用CAS操作: boolean compareAndSet(expectedValue, updateValue); 如果此方法(在不同的类间参数类型也不同)当前保持expectedValue,

Java并发编程相关面试问题

基础概念 1.什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)? 原子操作(atomic operation)意为"不可被中断的一个或一系列操作" .处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作. 在Java中可以通过锁和循环CAS的方式来实现原子操作. CAS操作--Compare & Set,或是 Compare & Swap,现在几乎所有的CPU指令都支持CAS的原子操作. 原子操作是

Java 并发编程学习笔记之核心理论基础_java

并发编程是Java程序员最重要的技能之一,也是最难掌握的一种技能.它要求编程者对计算机最底层的运作原理有深刻的理解,同时要求编程者逻辑清晰.思维缜密,这样才能写出高效.安全.可靠的多线程并发程序.本系列会从线程间协调的方式(wait.notify.notifyAll).Synchronized及Volatile的本质入手,详细解释JDK为我们提供的每种并发工具和底层实现机制.在此基础上,我们会进一步分析java.util.concurrent包的工具类,包括其使用方式.实现源码及其背后的原理.本

Java并发编程【1.2时代】

    本文介绍了Java原生的多线程技术(1.2),通过详细介绍wait和notify相关的机制.基础的多线程技术以及基于这些技术的等待超时.线程间的通信技术和线程池高阶技术,最后通过一个基于线程池的简单文本web服务器-MollyServer,来阐明多线程带来好处.通过介绍这些技术,展示了在没有使用Java并发包的时代(1.5-)是如何完成Java的多线程编程,为理解Java5提供了良好帮助. 线程简介        Java从诞生开始就明智的选择内置对多线程的支持,这将Java语言同其他同

Java并发开发:Lock框架详解

摘要: 我们已经知道,synchronized 是java的关键字,是Java的内置特性,在JVM层面实现了对临界资源的同步互斥访问,但 synchronized 粒度有些大,在处理实际问题时存在诸多局限性,比如响应中断等.Lock 提供了比 synchronized更广泛的锁操作,它能以更优雅的方式处理线程同步问题.本文以synchronized与Lock的对比为切入点,对Java中的Lock框架的枝干部分进行了详细介绍,最后给出了锁的一些相关概念. 一. synchronized 的局限性

Java并发编程之性能、扩展性和响应_java

本文讨论的重点在于多线程应用程序的性能问题.我们会先给性能和扩展性下一个定义,然后再仔细学习一下Amdahl法则.下面的内容我们会考察一下如何用不同的技术方法来减少锁竞争,以及如何用代码来实现. 1.性能 我们都知道,多线程可以用来提高程序的性能,背后的原因在于我们有多核的CPU或多个CPU.每个CPU的内核都可以自己完成任务,因此把一个大的任务分解成一系列的可彼此独立运行的小任务就可以提高程序的整体性能了.可以举个例子,比如有个程序用来将硬盘上某个文件夹下的所有图片的尺寸进行修改,应用多线程技

Java并发编程总结——慎用CAS详解_java

一.CAS和synchronized适用场景 1.对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源:而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能. 2.对于资源竞争严重的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized.以java.util.concurrent.atomic包中AtomicInteger类为例,其getAn

如何使用Contemplate ThreadSafe发现并判断Java并发问题

事实证明,要发挥多核硬件所带来的收益是很困难和有风险的.当使用并发正确和安全地编写Java软件时,我们需要很仔细地进行思考.因为错误使用并发会导致偶尔才出现的缺陷,这些缺陷甚至能够躲过最严格的测试环境. 静态分析工具提供了一种方式,可以在代码执行之前探查并修正并发错误.它能够在代码执行之前分析程序的源码或编译形成的字节码,进而发现隐藏在代码之中的缺陷. Contemplate的ThreadSafe Solo是一个商用的Eclipse静态分析插件,其目的就是专门用来发现并诊断隐藏在Java程序之中