我的目标是实现一个对Widgets进行操作的服务,该服务具有属性name
。该服务的操作由多个较小的步骤组成,这些步骤应按name
同步执行,这意味着不能对同名的小部件进行两次操作交织。
最简单的解决方案是完全锁定这些操作,例如使用这样的EJB:
@Singleton
public class WidgetService {
@Lock(LockType.WRITE)
public void doThing(Widget widget) {
// many individual steps
}
}
但是,这也意味着对不同名称的小部件的操作不能并行执行,这会严重影响我的性能。所以我的解决方案是实现一个名字锁定机制。 Lock本身看起来大致如下:
public class NameLock implements AutoCloseable {
private static final ConcurrentMap<String, ReentrantReadWriteLock> globalLocks = new ConcurrentHashMap<>();
private final String name;
private NameLock(String name) {
this.name = name;
final ReentrantReadWriteLock lock = globalLocks.computeIfAbsent(name, s -> new ReentrantReadWriteLock());
lock.writeLock().lock();
}
@Override
public void close() {
final ReentrantReadWriteLock lock = globalLocks.get(name);
lock.writeLock().unlock();
}
}
并使用如下:
public class WidgetService {
public void doThing(Widget widget) {
try (NameLock lock = new NameLock(widget.getName())) {
// many individual steps
}
}
}
但是,这意味着globalLocks
地图会填充永远不会被清理的锁。我不能简单地在close方法中调用globalLocks.remove(name)
,因为由于并行性,另一个线程可能已经检索到该锁并且在lock()
方法被阻止。有一种优雅的方式,我可以通过线程安全,非内存泄漏的方式实现这一目标吗?
我为此编写了一个测试,如果锁定按预期工作,它应该快速完成而不会出错。这传递给我的实现,但仍然在地图中留下锁:
@Test
public void lockMultiple() throws Exception {
final int nThreads = 10;
final int nTasks = 200;
final Random random = new Random();
final ExecutorService threads = Executors.newFixedThreadPool(nThreads);
final Function<Integer, Integer> func = i -> {
try (NameLock lock = new NameLock("foo")) {
// fuzzing
Thread.sleep(random.nextInt(10));
}
return i;
};
final List<Future<Integer>> futures = IntStream.range(0, nTasks)
.mapToObj(i -> (Callable<Integer>) () -> func.apply(i))
.map(threads::submit)
.collect(Collectors.toList());
for (Future<Integer> future : futures) {
final Integer i = future.get(10, TimeUnit.SECONDS);
}
}