重入锁reentrantlock 可以选择 公平锁和非公平锁。
内部锁synchronized 本身是 非公平锁。
从性能的角度上看, 非公平锁性能要远大于公平锁。
至于重入锁中非公平锁的性能和 内部锁synchronized的性能比较并没有谁优谁劣的情况。
以下例子是对 三者的性能对比:
下面具体的构造一个测试程序来具体考察 ReentrantLock 的性能。构造一个 计数器 Counter,启动 N 个线程对计数器进行递增操作。显然,这个递增操作需 要同步以防止数据冲突和线程干扰,为保证原子性,采用 3 种锁来实现同步,然 后查看结果。
测试环境是双核酷睿处理器,内存 3G,JDK6。
第一种是内在锁,第二种是不公平的 ReentrantLock 锁,第三种是公平的 ReentrantLock 锁。
首先定义一个计数器接口。
package locks;
public interface Counter {
public long getValue();
public void increment();
}
下面是使用内在锁的计数器类:
package lockbenchmark;
public class SynchronizedCounter implements Counter {
private long count = 0;
public long getValue() {
return count;
}
public synchronized void increment() {
count++;
}
}
下面是使用不公平 ReentrantLock 锁的计数器。
package lockbenchmark;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantUnfairCounterLockCounter implements Counter {
private volatile long count = 0;
private Lock lock;
public ReentrantUnfairCounterLockCounter() {
// 使用非公平锁,true就是公平锁
lock = new ReentrantLock(false);
}
public long getValue() {
return count;
}
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
下面是使用公平的 ReentrantLock 锁的计数器。
package lockbenchmark;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantFairLockCounter implements Counter {
private volatile long count = 0;
private Lock lock;
public ReentrantFairLockCounter() {
// true就是公平锁
lock = new ReentrantLock(true);
}
public long getValue() {
return count;
}
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
下面是总测试程序。
package lockbenchmark;
import java.util.concurrent.CyclicBarrier;
public class BenchmarkTest {
private Counter counter;
// 为了统一启动线程,这样好计算多线程并发运行的时间
private CyclicBarrier barrier;
private int threadNum;// 线程数
private int loopNum;// 每个线程的循环次数
private String testName;
public BenchmarkTest(Counter counter, int threadNum, int loopNum, String testName) {
this.counter = counter;
barrier = new CyclicBarrier(threadNum + 1); // 关卡计数=线程数
this.threadNum = threadNum;
this.loopNum = loopNum;
this.testName = testName;
}
public static void main(String args[]) throws Exception {
int threadNum = 2000;
int loopNum = 1000;
new BenchmarkTest(new SynchronizedCounter(), threadNum, loopNum, "内部锁").test();
new BenchmarkTest(new ReentrantUnfairCounterLockCounter(), threadNum, loopNum, "不公平重入锁").test();
new BenchmarkTest(new ReentrantFairLockCounter(), threadNum, loopNum, "公平重入锁").test();
}
public void test() throws Exception {
try {
for (int i = 0; i < threadNum; i++) {
new TestThread(counter, loopNum).start();
}
long start = System.currentTimeMillis();
barrier.await(); // 等待所有任务线程创建,然后通过关卡,统一执行
barrier.await(); // 等待所有任务计算完成
long end = System.currentTimeMillis();
System.out.println(this.testName + " count value:" + counter.getValue());
System.out.println(this.testName + " 花费时间:" + (end - start) + "毫秒");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
class TestThread extends Thread {
int loopNum = 100;
private Counter counter;
public TestThread(final Counter counter, int loopNum) {
this.counter = counter;
this.loopNum = loopNum;
}
public void run() {
try {
barrier.await();// 等待所有的线程开始
for (int i = 0; i < this.loopNum; i++)
counter.increment();
barrier.await();// 等待所有的线程完成
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
对三种锁分别设置两个不同的参数:不同线程数和每个线程数的循环次数。 最后记录每种锁的运行时间(单位:ms),形成下表。
循环次数 1000
线程数 | 200 | 500 | 1000 | 2000 |
---|---|---|---|---|
内在锁 | 62 | 313 | 406 | 875 |
非公平锁 | 32 | 94 | 250 | 859 |
公平锁 | 4641 | 17610 | 44671 | 57391 |
循环次数 200
线程数 | 200 | 500 | 1000 | 2000 |
---|---|---|---|---|
内在锁 | 47 | 94 | 109 | 265 |
非公平锁 | 16 | 32 | 125 | 906 |
公平锁 | 781 | 3031 | 8671 | 13625 |
分析统计结果,在线程数小于 2000 的情况下,非公平可重入锁的性能要优 于内部锁。公平可重入锁的性能最差。同时发现内部锁其实也是一个非公平锁。
重入锁(ReentrantLock)与内部锁在加锁和内存语义上是相同的。从性能上看,重入锁的性能看起来胜过内部锁。在 Java 5.0 中,两者性能之间的差距比较 大;而在 Java 6 中,这种差距变得比较小。与重入锁相比,内部锁仍然具有很大 的优势,比如内部锁更为人们所熟悉,也更简洁,而且很多现有的程序已经在使 用内部锁了。重入锁是很危险的同步工具,程序员在使用重入锁时,容易产生错 误。因此,只有在内部锁不能满足需求,才需要使用重入锁。
在 Java 5.0 中,内部锁还具有另外一个优点:线程转储能够显示哪些调用框 架获得了哪些锁,并能够识别发生了死锁的线程。但 Java 虚拟机并不知道哪个 线程持有重入锁,因此在调试使用了重入锁的线程时,无法从中获得帮助信息。 这个问题在 Java 6 中得到了解决,它提供了一个管理和调试接口,锁可以使用这 个接口进行注册,并通过其他管理和调试接口,从线程转储中得到重入锁的加锁 信息。
由于内部锁是内置于 Java 虚拟机中的,它能够进行优化,因此未来的性能 改进可能更倾向于内部锁,而不是重入锁。综上所述,除非你的应用程序需要发布在 Java 5.0 上,或者需要使用重入锁的可伸缩性,否则就应该选择内部锁。
总之,ReentrantLock 锁与 Java 内在锁相比有下面的特点:
- ReentrantLock 必须在 finally 块中释放锁,而使用 synchronized 同步,JVM
将确保锁会获得自动释放。 - 与目前的 synchronized 实现相比,争用下的 ReentrantLock 实现更具可
伸缩性。 - 对于 ReentrantLock ,可以有不止一个条件变量与它关联。
- 允许选择想要一个公平锁,还是一个不公平锁。
- 除非您对 Lock 的某个高级特性有明确的需要,或者有明确的证据表明
在特定情况下,同步已经成为可伸缩性的瓶颈,否则还是应当继续使用 synchronized。 - Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。而 且,几乎每个开发人员都熟悉 synchronized,它可以在 JVM 的所有版本中工作。