我有一个共享的临时文件资源,它被分成4K块(或某些这样的值)。文件中的每个4K由从零开始的索引表示。对于此共享资源,我跟踪正在使用的4K块索引,并始终返回未使用的最低索引4K块,如果所有正在使用,则返回-1。
这个索引的ResourceSet类有一个公共的获取和释放方法,它们都使用同步锁,其持续时间大约相当于生成4个随机数(昂贵的,cpu-wise)。
因此,从下面的代码中可以看出,我使用AtomicInteger“计数信号量”来防止大量线程在acquire()上同时进入临界区,返回-1(不可用)现在)如果线程太多了。
目前,我使用100的常量作为紧密的CAS循环来尝试增加获取中的原子整数,并将最大线程数的常数设置为10然后允许进入临界区,这足够长创造争论。我的问题是,这些常量应该适用于中等到高负载的servlet引擎,它有几个线程试图访问这些4K块?
public class ResourceSet {
// ??? what should this be
// maximum number of attempts to try to increment with CAS on acquire
private static final int CAS_MAX_ATTEMPTS = 50;
// ??? what should this be
// maximum number of threads contending for lock before returning -1 on acquire
private static final int CONTENTION_MAX = 10;
private AtomicInteger latch = new AtomicInteger(0);
... member variables to track free resources
private boolean aquireLatchForAquire ()
{
for (int i = 0; i < CAS_MAX_ATTEMPTS; i++) {
int val = latch.get();
if (val == -1)
throw new AssertionError("bug in ResourceSet"); // this means more threads than can exist on any system, so its a bug!
if (!latch.compareAndSet(val, val+1))
continue;
if (val < 0 || val >= CONTENTION_MAX) {
latch.decrementAndGet();
// added to fix BUG that comment pointed out, thanks!
return false;
}
}
return false;
}
private void aquireLatchForRelease ()
{
do {
int val = latch.get();
if (val == -1)
throw new AssertionError("bug in ResourceSet"); // this means more threads than can exist on any system, so its a bug!
if (latch.compareAndSet(val, val+1))
return;
} while (true);
}
public ResourceSet (int totalResources)
{
... initialize
}
public int acquire (ResourceTracker owned)
{
if (!aquireLatchForAquire())
return -1;
try {
synchronized (this) {
... algorithm to compute minimum free resoource or return -1 if all in use
return resourceindex;
}
} finally {
latch.decrementAndGet();
}
}
public boolean release (ResourceIter iter)
{
aquireLatchForRelease();
try {
synchronized (this) {
... iterate and release all resources
}
} finally {
latch.decrementAndGet();
}
}
}
答案 0 :(得分:1)
编写一个优秀且高性能的自旋锁实际上非常复杂,需要很好地理解内存障碍。仅仅选择一个常数并不会削减它,绝对不会是便携式的。 Google的gperftools有an example你可以看一下,但可能比你需要的更复杂。
如果您真的想减少对锁的争用,您可能需要考虑使用更细粒度和乐观的方案。一个简单的方法是将你的块分成n组并将锁与每个组相关联(也称为剥离)。这将有助于减少争用并提高吞吐量,但无助于减少延迟。您还可以将AtomicBoolean与每个块和CAS相关联以获取它(在发生故障时重试)。在处理无锁算法时要小心,因为它们往往很难做到正确。如果你做对了,它可以大大减少获取块的延迟。
请注意,如果不知道块选择算法的样子,就很难提出更细粒度的方法。我还假设你确实遇到了性能问题(它已经被分析过了一切)。
虽然我在这,但你的自旋锁实现是有缺陷的。你永远不应该直接在CAS上旋转,因为你是在发送内存障碍。任何严重的争用(与thundering-herd problem相关)都会非常缓慢。最低限度是首先在CAS之前检查变量的可用性(如果没有屏障读取,则会很简单)。更好的方法是不要让所有的线程都在相同的值上旋转。这应该避免相关的缓存行在您的核心之间进行乒乓。
请注意,我不知道Java中的原子操作与哪种类型的内存屏障相关联,因此我的上述建议可能不是最佳或正确的。
最后,The Art Of Multiprocessor Programming是一本有趣的书,可以让我更好地了解我在这个答案中一直在喷出的所有非感觉。
答案 1 :(得分:0)
我不确定是否有必要为此方案伪造自己的Lock类。由于JDK提供了ReentrantLock,它还在锁定获取期间利用CAS指令。与个人锁类相比,性能应该相当不错。
答案 2 :(得分:0)
如果您希望线程对Semaphore
进行跟踪,则可以使用tryAcquire
的{{1}}方法。
我只需将no resource available
关键字替换为synchronized
,然后使用ReentrantLock
方法。如果你想让你的线程等一下,你可以在同一个类上使用tryLock()
。需要选择哪一个以及用于超时的值,需要通过性能测试来确定。
创建一个明确的门似乎对你来说似乎没什么用。我并不是说它永远无法帮助,但IMO实际上更有可能损害性能,而且肯定会增加复杂性。因此,除非你在这里遇到性能问题(基于你所做的测试),并且你发现这种门控有帮助,我建议采用最简单的实现。