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。

目录
相关文章
|
3月前
|
监控 Cloud Native Java
Quarkus 云原生Java框架技术详解与实践指南
本文档全面介绍 Quarkus 框架的核心概念、架构特性和实践应用。作为新一代的云原生 Java 框架,Quarkus 旨在为 OpenJDK HotSpot 和 GraalVM 量身定制,显著提升 Java 在容器化环境中的运行效率。本文将深入探讨其响应式编程模型、原生编译能力、扩展机制以及与微服务架构的深度集成,帮助开发者构建高效、轻量的云原生应用。
405 44
|
3月前
|
安全 Java API
Java Web 在线商城项目最新技术实操指南帮助开发者高效完成商城项目开发
本项目基于Spring Boot 3.2与Vue 3构建现代化在线商城,涵盖技术选型、核心功能实现、安全控制与容器化部署,助开发者掌握最新Java Web全栈开发实践。
403 1
|
4月前
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
159 4
|
4月前
|
Java API Maven
2025 Java 零基础到实战最新技术实操全攻略与学习指南
本教程涵盖Java从零基础到实战的全流程,基于2025年最新技术栈,包括JDK 21、IntelliJ IDEA 2025.1、Spring Boot 3.x、Maven 4及Docker容器化部署,帮助开发者快速掌握现代Java开发技能。
892 1
|
5月前
|
人工智能 Java
Java多任务编排技术
JDK 5引入Future接口实现异步任务处理,但获取结果不够灵活。Java 8新增CompletableFuture,实现异步任务编排,支持流式处理、多任务组合及异常处理,提升执行效率与代码可读性,简化并发编程复杂度。
134 0
|
2月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
89 4
|
2月前
|
缓存 安全 Java
如何理解Java中的并发?
Java并发指多任务交替执行,提升资源利用率与响应速度。通过线程实现,涉及线程安全、可见性、原子性等问题,需用synchronized、volatile、线程池及并发工具类解决,是高并发系统开发的关键基础。(238字)
240 5
|
4月前
|
Java 测试技术 API
2025 年 Java 开发者必知的最新技术实操指南全览
本指南涵盖Java 21+核心实操,详解虚拟线程、Spring Boot 3.3+GraalVM、Jakarta EE 10+MicroProfile 6微服务开发,并提供现代Java开发最佳实践,助力开发者高效构建高性能应用。
750 4
|
3月前
|
安全 Cloud Native Java
Java 模块化系统(JPMS)技术详解与实践指南
本文档全面介绍 Java 平台模块系统(JPMS)的核心概念、架构设计和实践应用。作为 Java 9 引入的最重要特性之一,JPMS 为 Java 应用程序提供了强大的模块化支持,解决了长期存在的 JAR 地狱问题,并改善了应用的安全性和可维护性。本文将深入探讨模块声明、模块路径、访问控制、服务绑定等核心机制,帮助开发者构建更加健壮和可维护的 Java 应用。
288 0
|
4月前
|
JavaScript 安全 前端开发
Java开发:最新技术驱动的病人挂号系统实操指南与全流程操作技巧汇总
本文介绍基于Spring Boot 3.x、Vue 3等最新技术构建现代化病人挂号系统,涵盖技术选型、核心功能实现与部署方案,助力开发者快速搭建高效、安全的医疗挂号平台。
257 3