基于方法参数的细粒度同步/锁定方法调用

7
我希望能够基于某个ID来同步方法调用,类似于给定对象实例的并发装饰器。
例如:
所有调用参数为“id1”的方法的线程应该按顺序执行。
所有调用具有不同参数(例如“id2”)的方法的线程应该与调用参数为“id1”的方法的线程并行执行,但是再次按顺序执行彼此之间的线程。
因此,在我的想法中,可以通过每个方法参数拥有一个锁(http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/locks/ReentrantLock.html)实例来实现。每次使用该参数调用方法时,将查找对应于特定参数值(例如“id1”)的锁实例,并尝试获取锁的当前线程。
在代码中可以这样表示:
public class ConcurrentPolicyWrapperImpl implements Foo {

private Foo delegate;

/**
* Holds the monitor objects used for synchronization.
*/
private Map<String, Lock> concurrentPolicyMap = Collections.synchronizedMap(new HashMap<String, Lock>());

/**
* Here we decorate the call to the wrapped instance with a synchronization policy.
*/
@Override
public Object callFooDelegateMethod (String id) {
        Lock lock = getLock(id);
        lock.lock();
        try {
            return delegate.delegateMethod(id);
        } finally {
            lock.unlock();
        }
}


protected Lock getLock(String id) {
        Lock lock = concurrentPolicyMap.get(id);

        if (lock == null) {
            lock = createLock();
            concurrentPolicyMap.put(id, lock);

        }
        return lock;
    }

}

protected Lock createLock() {
        return new ReentrantLock();
    }

看起来这个方案可行 - 我用jmeter等工具进行了一些性能测试。不过,正如我们都知道的那样,Java中的并发是一个棘手的问题,我决定在这里征求您的意见。

我一直在想,可能有更好的方法来实现这个目标。例如,使用其中一个BlockingQueue实现。您觉得呢?

我也无法确定在获取锁即protected Lock getLock(String id)方法时是否存在潜在的同步问题。我正在使用同步集合,但这是否足够呢?换句话说,应该像下面这样而不是我当前的方式:

protected Lock getLock(String id) {
  synchronized(concurrentPolicyMap) {
    Lock lock = concurrentPolicyMap.get(id);

    if (lock == null) {
      lock = createLock();
      concurrentPolicyMap.put(id, lock);

    }
    return lock;
  }

}

那么你们认为呢?

2个回答

4
除了创建锁的问题外,这个模式是可以的,但是您可能会有无限数量的锁。通常人们通过创建/使用分段锁来避免这种情况。在guava库中有一个良好/简单的实现。 锁分段的应用领域 如何通过关键字获取锁 http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/util/concurrent/Striped.html 使用guava实现的示例代码:
private Striped<Lock> STRIPPED_LOCK = Striped.lock(64);

public static void doActualWork(int id) throws InterruptedException {
    try {
        STRIPPED_LOCK.get(id).lock();
        ...
    } finally {
        STRIPPED_LOCK.get(id).unlock();
    }
}

你说“暂且不谈锁的创建问题”……你是否发现了任何问题,或者是指你不想对此发表评论? - Svilen
现在更仔细地看这个问题 - 是的,你肯定需要在 getLock() 方法中使用同步块。就像你在第二个示例中所写的那样。否则,假设(a)5个线程同时调用具有相同ID的getLock()方法; (b)他们都发现锁为null; (c)他们都创建并返回新的锁实例。失败。你第二段代码中的同步块避免了这种情况的发生。 - Keith
谢谢,Keith。顺便问一下,你有没有想过如何使用执行器/任务来完成类似的事情呢?即使用单个ThreadPoolExecutor,但按键分区任务执行。例如,针对键“id1”的任务将串行运行到彼此之间,同时与键“id2”、“id3”等的所有其他任务并行运行... - Svilen
回答自己的评论...这是关于这个主题的一篇不错的文章:http://www.javaspecialists.eu/archive/Issue206.html 此外,在同一篇文章中提到了一些Executor javadocs中的提示,例如SerialExecutor。 - Svilen
@Keith,你对你的实现非常确定吗?因为你没有持有锁对象的强引用,而是在finally块中再次引用了STRIPED_LOCK。我问这个问题是因为使用lazyWeakLock意味着当没有强引用时可以回收锁(似乎与其锁定状态无关)。因此,我认为你的示例代码可能不是线程安全的。 - Trevor Freeman
@increment1 很好的发现。我已经更新了示例,使用 'Striped.lock(int)' 方法而不是 'Striped.lazyWeakLock(int)',因为弱引用并不是这里所展示的内容。 - Keith

1
尽管我个人更喜欢Keith建议的Guava的Striped方法,但为了讨论和完整性,我想指出使用动态代理或更通用的AOP(面向切面编程)是一种方法。因此,我们将定义一个IStripedConcurrencyAware接口,它将作为您所需的"类似于并发装饰器的东西",基于这个接口的动态代理/AOP方法劫持将将方法调用解复用到适当的Executor/Thread中。我个人不喜欢AOP(或大多数Spring),因为它破坏了核心Java的所见即所得的简单性,但你的情况可能有所不同。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接