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