简体   繁体   English

如何在Java中创建时正确锁定资源

[英]How to properly lock a resource while created in Java

I have this singleton that creates objects, currently it looks like this: 我有这个创建对象的单例,目前它看起来像这样:

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);
     }
 }

The obvious problem it has is, if two threads try to access the same thing, they may endup duplicating the thing. 它有一个明显的问题是,如果两个线程试图访问同一个东西,它们可能会最终复制该东西。

So I used a lock: 所以我用了一把锁:

 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);
      }
 }

But now that's worst because I'll be locking the map for a while each time a new resource is being created in demerit of the other threads wanting different things. 但现在这是最糟糕的,因为每次创建一个新资源时,我会锁定地图一段时间,因为其他线程需要不同的东西。

I'm pretty sure it can be better with Java 5 concurrent package. 我很确定使用Java 5并发包可以更好。 Can somebody point me in the right direction? 有人能指出我正确的方向吗?

What I want to avoid is to lock the class or the map for other threads that are interested in other things. 我想要避免的是锁定其他线程感兴趣的其他线程的类或映射。

If you want to prevent creating the item multiple times which also blocking as little as possible, I would use two maps. 如果你想防止多次创建项目,尽可能少阻塞,我会使用两个地图。 One for holding a set of locks for use when creating the object and one for holding the objects. 一个用于保存一组锁,以便在创建对象时使用,另一个用于保存对象。

Consider something like this: 考虑这样的事情:

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);
}

This will only block multiple thread attempting to get the same key, while preventing the Thing being created twice for the same key. 这只会阻止尝试获取相同密钥的多个线程,同时防止为同一个密钥创建两次Thing

Update 更新

Guava Cache example moved to new Answer. Guava Cache示例转移到新的答案。

Maybe ConcurrentHashMap can help you. 也许ConcurrentHashMap可以帮到你。 As its name implies, it supports concurrent modifications. 顾名思义,它支持并发修改。

To create a new element only once, you could do something like this: 要只创建一个新元素,您可以执行以下操作:

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;
}

Only one thread is allowed to create new stuff at a time, but for existing values there is no locking whatsoever. 一次只允许一个线程创建新的东西,但是对于现有的值,没有任何锁定。

If you want to avoid locks completely you'd have to use 2 maps but it gets somewhat convoluted and it's only worth it if you really expect many threads to be populating the map continually. 如果你想完全避免锁定,你必须使用2个地图,但它有点令人费解,如果你真的希望很多线程不断地填充地图,那么它是值得的。 For that case it might be better to use FutureTasks together with a thread pool to create the objects asynchronously, minimizing the time the lock is in place (you still need a lock so that only one thread creates the new element). 对于这种情况,最好将FutureTasks与线程池一起使用,以异步方式创建对象,最大限度地减少锁定的时间(您仍需要锁定,以便只有一个线程创建新元素)。

The code would be something like this: 代码将是这样的:

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();
}

The lock will only be in place for the time it takes to create the Callable, submit it to the thread pool to get a Future, and put that Future in the map. 锁定仅在创建Callable所需的时间内存在,将其提交到线程池以获取Future,并将Future置于地图中。 The Callable will create the element in the thread pool and when it returns the element, the get() method of the Future will unlock and return its value (for any threads that are waiting; subsequent calls won't lock). Callable将在线程池中创建元素,当它返回元素时,Future的get()方法将解锁并返回其值(对于任何等待的线程;后续调用不会锁定)。

After looking into it, I thing Guava's LoadingCache is probably a very nice solution for this. 在研究之后,我认为Guava's LoadingCache可能是一个非常好的解决方案。 By default the CacheBuilder will create a cache that does not do any eviction (so it is just a map) and has the thread-blocking when loading for a key already built in. 默认情况下, CacheBuilder将创建一个不执行任何驱逐的缓存(因此它只是一个映射),并且在加载已经内置的密钥时具有线程阻塞功能。

LoadingCache LoadingCache

CacheBuilder 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);
  }

You could immediatley insert a lightweight proxy in the map. 你可以immediatley在地图中插入一个轻量级代理。 The proxy will delegate to the real object once it is initialized, but block until then. 一旦初始化,代理将委托给真实对象,但在此之前阻塞。 Once the real Thing is initialized, it can replace the proxy in the map. 一旦真正的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;
    }
}

This locks on the map, but only briefly to create a proxy and put it if needed. 这会锁定地图,但只是简单地创建一个代理并在需要时放置它。

The code for the proxy may become laborious, in which case you may be better of creating a proxy using reflection (see java.lang.reflect.Proxy ) 代理的代码可能变得费力,在这种情况下,您可能更好地使用反射创建代理(请参阅java.lang.reflect.Proxy

You could use a ConcurrentHashMap . 您可以使用ConcurrentHashMap This one can be accessed by multiple threads without problem. 这个可以被多个线程访问而没有问题。

I tried these solutions and they failed at some point in my implementation, not saying they wont work in other scenarios. 我尝试了这些解决方案,但是在我的实现中他们失败了,并没有说他们不会在其他场景中工作。

What I finally end up doing was to use a ConcurrentMap to check if the resource has been already requested or not. 我最终做的是使用ConcurrentMap来检查资源是否已被请求。 If not, it is created and stored somewhere else. 如果没有,它将被创建并存储在其他地方。

... 
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() );
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM