4

我有这个创建对象的单例,目前它看起来像这样:

public ApplicationManagerSingleton {
    ....
    private Map<String, Thing> map = new HashMap<String, Thing>();

    public Thing getThingById( String id ) {
       Thing t = null;
       if ( !map.contains(id) ) {
        t = longAndCostlyInitializationOfThing();
        map.put(id, t );
       }
       return map.get(id);
     }
 }

它的一个明显问题是,如果两个线程试图访问同一个东西,它们最终可能会复制这个东西。

所以我用了一个锁:

 public ApplicationManagerSingleton {
      private Map<String, Thing> map = new HashMap<Sring, Thing>();
      public Thing getThingById(String id ) {
          synchronized( map ) {
             if (!map.contains(id)) {
                 t = initialize....
             }
             map.put(id, t);
           }
           returns map.get(id);
      }
 }

但现在这是最糟糕的,因为每次创建新资源时我都会锁定地图一段时间,因为其他线程想要不同的东西。

我很确定使用 Java 5 并发包会更好。有人可以指出我正确的方向吗?

我要避免的是锁定对其他事物感兴趣的其他线程的类或映射。

4

6 回答 6

3

如果您想防止多次创建该项目也尽可能少地阻止,我会使用两个地图。一个用于持有一组锁以在创建对象时使用,一个用于持有对象。

考虑这样的事情:

private ConcurrentMap<String, Object> locks = new ConcurrentHashMap<String, Object>();
private ConcurrentMap<String, Thing> things = new ConcurrentHashMap<String, Thing>();

public void Thing getThingById(String id){
    if(!things.containsKey(id)){        
      locks.putIfAbsent(id, new Object());
      synchronized(locks.get(id)){
         if (!things.containsKey(id)){
             things.put(id, createThing());
         }
      }
    }

    return things.get(id);
}

这只会阻止多个线程尝试获取相同的密钥,同时防止Thing为同一密钥创建两次。

更新

番石榴缓存示例移至新答案。

于 2013-01-04T18:15:57.543 回答
3

也许 ConcurrentHashMap 可以帮助你。顾名思义,它支持并发修改。

要只创建一次新元素,您可以执行以下操作:

private Map<String,Thing> map = new ConcurrentHashMap<>();
private final Object lock = new Object();
public Thing getById(String id) {
  Thing t = map.get(id);
  if (t == null) {
    synchronized(lock) {
      if (!map.containsKey(id)) {
        t = //create t
        map.put(id, t);
      }
    }
  }
  return t;
}

一次只允许一个线程创建新内容,但对于现有值,没有任何锁定。

如果您想完全避免锁定,则必须使用 2 个映射,但它会有些复杂,只有当您真的希望许多线程不断填充映射时才值得。对于这种情况,最好将 FutureTasks 与线程池一起使用来异步创建对象,从而最大限度地减少锁定到位的时间(您仍然需要一个锁定,以便只有一个线程创建新元素)。

代码将是这样的:

private Map<String,Future<Thing>> map = new ConcurrentHashMap<>();
private final Object lock = new Object();
ExecutorService threadPool = ...;
public Thing getById(String id) {
  Future<Thing> t = map.get(id);
  if (t == null) {
    synchronized(lock) {
      if (!map.containsKey(id)) {
        Callable<Thing> c = //create a Callable that creates the Thing
        t = threadPool.submit(c);
        map.put(id, t);
      }
    }
  }
  return t.get();
}

锁只会在创建 Callable、将其提交到线程池以获取 Future 并将该 Future 放入映射所需的时间内到位。Callable 将在线程池中创建元素,当它返回元素时,Future 的 get() 方法将解锁并返回其值(对于任何正在等待的线程;后续调用不会锁定)。

于 2013-01-04T18:32:14.157 回答
2

在调查之后,我认为这Guava's LoadingCache可能是一个非常好的解决方案。默认情况下,CacheBuilder将创建一个不执行任何驱逐的缓存(因此它只是一个映射),并且在加载已内置的键时具有线程阻塞。

加载缓存

缓存生成器

 private Cache<String, Thing> myCache;

 MyConstructor(){
    myCache = CacheBuilder.newBuilder().build(
       new CacheLoader<String, Thing>() {
         public Thing load(String key) throws AnyException {
           return createExpensiveGraph(key);
         }
        });
 }

  public void Thing getThingById(String id){
    return myCache.get(id);
  }
于 2013-01-04T20:27:50.420 回答
1

您可以立即在地图中插入轻量级代理。一旦初始化,代理将委托给真实对象,但在此之前一直阻塞。一旦实体Thing被初始化,它就可以替换地图中的代理。

private Map<String, Thing> map = new ConcurrentHashMap<>();

public Thing getThingById(String id) {
    ThingProxy proxy = null;
    synchronized (map) {
        if (!map.containsKey(id)) {
            proxy = new ThingProxy();
            map.put(id, proxy);
        }
    }
    if (proxy != null) {
        proxy.initialize();
        map.put(id, proxy.getRealThing());
    }
    return map.get(id);
}

private class ThingProxy implements Thing {

    private Thing realThing;
    private CountDownLatch countDownLatch = new CountDownLatch(1);

    @Override
    public void someMethodOfThing() {
        try {
            countDownLatch.await();
            realThing.someMethodOfThing();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public void initialize() {
        realThing = longAndCostlyInitializationOfThing();
        countDownLatch.countDown();
    }

    public Thing getRealThing() {
        return realThing;
    }
}

这会锁定在地图上,但只是短暂地创建代理并在需要时放置它。

代理的代码可能会变得很费力,在这种情况下,您最好使用反射创建代理(请参阅 参考资料java.lang.reflect.Proxy

于 2013-01-04T18:10:27.423 回答
1

你可以使用一个ConcurrentHashMap. 这个可以被多个线程访问而没有问题。

于 2013-01-04T18:12:44.557 回答
0

我尝试了这些解决方案,但它们在我的实施中的某个时候失败了,并不是说它们在其他场景中不起作用。

我最终做的是使用ConcurrentMap检查资源是否已被请求。如果没有,它会被创建并存储在其他地方。

... 
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;

... 
private ConcurrentMap<String, Boolean> created = new ConcurrentMap<>();
....
if ( created.putIfAbsent( id, Boolean.TRUE ) == null ) {
    somewhereElse.put( id, createThing() );
}
于 2013-01-15T20:55:33.960 回答