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。

目录
相关文章
|
19天前
|
NoSQL Java 数据库连接
深入探索 Java 后台开发的核心技术
【4月更文挑战第5天】本文探讨了Java后台开发的关键技术,包括Spring框架与Spring Boot的使用,MyBatis和Hibernate的ORM选择,关系型与NoSQL数据库的适用场景,线程池与异步处理在并发中的作用,微服务架构及RESTful API设计。这些核心技术有助于开发者打造稳定、高性能的Java后台系统,适应不断发展的云计算和人工智能需求。
|
25天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
68 0
|
5天前
|
安全 Java
深入理解 Java 多线程和并发工具类
【4月更文挑战第19天】本文探讨了Java多线程和并发工具类在实现高性能应用程序中的关键作用。通过继承`Thread`或实现`Runnable`创建线程,利用`Executors`管理线程池,以及使用`Semaphore`、`CountDownLatch`和`CyclicBarrier`进行线程同步。保证线程安全、实现线程协作和性能调优(如设置线程池大小、避免不必要同步)是重要环节。理解并恰当运用这些工具能提升程序效率和可靠性。
|
6天前
|
Java 关系型数据库 MySQL
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
UWB (ULTRA WIDE BAND, UWB) 技术是一种无线载波通讯技术,它不采用正弦载波,而是利用纳秒级的非正弦波窄脉冲传输数据,因此其所占的频谱范围很宽。一套UWB精确定位系统,最高定位精度可达10cm,具有高精度,高动态,高容量,低功耗的应用。
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
|
7天前
|
存储 数据可视化 安全
Java全套智慧校园系统源码springboot+elmentui +Quartz可视化校园管理平台系统源码 建设智慧校园的5大关键技术
智慧校园指的是以物联网为基础的智慧化的校园工作、学习和生活一体化环境,这个一体化环境以各种应用服务系统为载体,将教学、科研、管理和校园生活进行充分融合。无处不在的网络学习、融合创新的网络科研、透明高效的校务治理、丰富多彩的校园文化、方便周到的校园生活。简而言之,“要做一个安全、稳定、环保、节能的校园。
32 6
|
7天前
|
Java 开发者
Java中多线程并发控制的实现与优化
【4月更文挑战第17天】 在现代软件开发中,多线程编程已成为提升应用性能和响应能力的关键手段。特别是在Java语言中,由于其平台无关性和强大的运行时环境,多线程技术的应用尤为广泛。本文将深入探讨Java多线程的并发控制机制,包括基本的同步方法、死锁问题以及高级并发工具如java.util.concurrent包的使用。通过分析多线程环境下的竞态条件、资源争夺和线程协调问题,我们提出了一系列实现和优化策略,旨在帮助开发者构建更加健壮、高效的多线程应用。
7 0
|
7天前
|
存储 缓存 安全
Java并发基础之互斥同步、非阻塞同步、指令重排与volatile
在Java中,多线程编程常常涉及到共享数据的访问,这时候就需要考虑线程安全问题。Java提供了多种机制来实现线程安全,其中包括互斥同步(Mutex Synchronization)、非阻塞同步(Non-blocking Synchronization)、以及volatile关键字等。 互斥同步(Mutex Synchronization) 互斥同步是一种基本的同步手段,它要求在任何时刻,只有一个线程可以执行某个方法或某个代码块,其他线程必须等待。Java中的synchronized关键字就是实现互斥同步的常用手段。当一个线程进入一个synchronized方法或代码块时,它需要先获得锁,如果
24 0
|
7天前
|
监控 前端开发 算法
Java技术体系
Java技术体系(韩顺平老师整理)
9 0
|
16天前
|
存储 缓存 安全
【企业级理解】高效并发之Java内存模型
【企业级理解】高效并发之Java内存模型
|
18天前
|
存储 安全 Java
Java中实现高效的字符串拼接技术
【4月更文挑战第6天】在Java编程中,字符串拼接是一个常见的操作。然而,由于字符串的不可变性,频繁的拼接操作可能会导致性能问题。本文将探讨Java中实现高效字符串拼接的技术,包括使用StringBuilder类、StringBuffer类以及Java 8中的StringJoiner类。通过对比这些技术的优缺点,我们将为您提供在不同场景下选择合适的字符串拼接方法的建议。