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