我们都使用第三方库作为开发的正常部分。
通常,我们无法控制其内部。
JDK随附的库是一个典型示例。
这些库中的许多库都使用锁来管理竞争。
JDK锁具有两种实现。
人们使用原子CAS样式指令来管理索赔过程。
CAS指令往往是最昂贵的CPU指令类型,并且在x86上具有内存排序语义。
锁通常是无竞争的,这会导致可能的优化,从而可以使用避免使用原子指令的技术将锁偏向无竞争的线程。
这种偏向使得理论上的锁定可以被同一线程快速重新获得。
如果该锁最终被多个线程争用,则该算法将从偏见中恢复过来,并使用原子指令退回到标准方法。
偏向锁定已成为Java 6的默认锁定实现 。
在遵守单一作者原则时,偏向锁定应该是您的朋友。
最近,当使用套接字API时,我决定衡量锁定成本,并对结果感到惊讶。
我发现我的无竞争线程所产生的开销比我预期的要多。
我汇总了以下测试,以比较Java 6中可用的当前锁实现的成本。
考试
为了进行测试,我将在锁中增加一个计数器,并增加锁中竞争线程的数量。
对于Java可用的3种主要锁实现,将重复此测试:
- Java语言监视器上的原子锁定
- Java语言监视器上的偏向锁定
- Java 5中随java.util.concurrent包引入的ReentrantLock 。
我还将在最新的3代Intel CPU上运行测试。
对于每个CPU,我将执行测试,直到核心计数将支持的最大并发线程数为止。
该测试是在64位Linux(Fedora Core 15)和Oracle JDK 1.6.0_29上进行的。
编码
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.CyclicBarrier;
import static java.lang.System.out;
public final class TestLocks implements Runnable
{
public enum LockType { JVM, JUC }
public static LockType lockType;
public static final long ITERATIONS = 500L * 1000L *1000L;
public static long counter = 0L;
public static final Object jvmLock = new Object();
public static final Lock jucLock = new ReentrantLock();
private static int numThreads;
private static CyclicBarrier barrier;
public static void main(final String[] args) throws Exception
{
lockType = LockType.valueOf(args[0]);
numThreads = Integer.parseInt(args[1]);
runTest(numThreads); // warm up
counter = 0L;
final long start = System.nanoTime();
runTest(numThreads);
final long duration = System.nanoTime() - start;
out.printf("%d threads, duration %,d (ns)\n", numThreads, duration);
out.printf("%,d ns/op\n", duration / ITERATIONS);
out.printf("%,d ops/s\n", (ITERATIONS * 1000000000L) / duration);
out.println("counter = " + counter);
}
private static void runTest(final int numThreads) throws Exception
{
barrier = new CyclicBarrier(numThreads);
Thread[] threads = new Thread[numThreads];
for (int i = 0; i < threads.length; i++)
{
threads[i] = new Thread(new TestLocks());
}
for (Thread t : threads)
{
t.start();
}
for (Thread t : threads)
{
t.join();
}
}
public void run()
{
try
{
barrier.await();
}
catch (Exception e)
{
// don't care
}
switch (lockType)
{
case JVM: jvmLockInc(); break;
case JUC: jucLockInc(); break;
}
}
private void jvmLockInc()
{
long count = ITERATIONS / numThreads;
while (0 != count--)
{
synchronized (jvmLock)
{
++counter;
}
}
}
private void jucLockInc()
{
long count = ITERATIONS / numThreads;
while (0 != count--)
{
jucLock.lock();
try
{
++counter;
}
finally
{
jucLock.unlock();
}
}
}
}
编写测试脚本:
设置-x
对于{1..8}中的i; 做Java -XX:-UseBiasedLocking TestLocks JVM $ i; 做完了 对于{1..8}中的i; 做Java -XX:+ UseBiasedLocking TestLocks JVM $ i; 做完了 对于{1..8}中的i; 做Java TestLocks JUC $ i; 做完了
结果
图1 |
图2 |
图3 |
在现代英特尔处理器上,偏置锁定不再应该是默认的锁定实现。
我建议您使用-XX:-UseBiasedLocking JVM选项来评估您的应用程序和实验,以确定是否可以从针对无竞争情况使用基于原子锁的算法中受益。
观察结果
- 在无竞争的情况下,有偏锁比原子锁贵10%。 似乎对于最近的CPU代来说,原子指令的成本比偏向锁的必要内务处理要少。 在Nehalem之前,锁定指令会在内存总线上声明一个锁定以执行这些原子操作,每条操作将花费100个以上的周期。 自Nehalem以来,原子指令可以在CPU内核本地进行处理,并且在执行内存排序语义时不需要等待存储缓冲区为空时,通常只需花费10-20个周期。
- 随着争用的增加,语言监视器锁定将Swift达到吞吐量限制,而与线程数无关。
- 与使用同步的语言监视器相比,ReentrantLock提供了最佳的无竞争性能,并且随着争用的增加,扩展性也显着提高。
- 当2个线程竞争时,ReentrantLock具有降低性能的奇怪特征。 这值得进一步调查。
- 当竞争线程数较少时,Sandybridge遭受原子指令增加的延迟 ,这在上一篇文章中已详细介绍。 随着竞争线程数的不断增加,内核仲裁的成本趋于占主导地位,而Sandybridge则显示出其在提高内存吞吐量方面的优势。
结论
在开发自己的并发库时,如果无锁替代算法不是可行的选择,则建议使用ReentrantLock而不是使用synced关键字,因为它在x86上具有明显更好的性能。
更新2011年11月20日
Dave Dice指出,未对JVM启动的前几秒中创建的锁实施偏向锁。
我将在本周重新运行测试并发布结果。
我收到了更多质量反馈,表明我的结果可能无效。
微型基准测试可能会很棘手,但是在大型应用中衡量自己的应用程序的建议仍然存在。
考虑到Dave的反馈,可以在此后续博客中查看测试的重新运行。
翻译自: https://www.javacodegeeks.com/2012/07/java-lock-implementations.html