Java中的线程安全多例模式

13

假设有以下的多例实现:

public class Multiton 
{
    private static final Multiton[] instances = new Multiton[...];

    private Multiton(...) 
    {
        //...
    }

    public static Multiton getInstance(int which) 
    {
        if(instances[which] == null) 
        {
            instances[which] = new Multiton(...);
        }

        return instances[which];
    }
}

如何在不使用昂贵的getInstance()方法同步和双重检查锁定的争议的情况下保持线程安全和惰性?单例的一种有效方式在这里提到,但似乎不能扩展到多例。


3
你知道吗,我听说有一种技术叫做“永不使用全局可变状态”(除非框架强制要求)。 - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
5
你有没有证据表明在你的应用程序中,“昂贵”(但确实很容易做到正确)的getInstance方法同步实际上会产生重大影响? - Jon Skeet
不,这主要是一种学术练习。由于Singleton同步似乎已经成为一个有争议的话题,我想知道内部类解决方案是否以任何方式扩展到Multiton。 - donnyton
5个回答

18

更新:使用Java 8,甚至可以更简单:

public class Multiton {
    private static final ConcurrentMap<String, Multiton> multitons = new ConcurrentHashMap<>();

    private final String key;
    private Multiton(String key) { this.key = key; }

    public static Multiton getInstance(final String key) {
        return multitons.computeIfAbsent(key, Multiton::new);
    }
}

嗯,这很好!


原始回答

这是建立在JCiP中描述的记忆化模式基础上的解决方案。它使用类似于其他答案中使用的ConcurrentHashMap,但它不是直接存储Multiton实例,因为这可能会导致创建未使用的实例,而是存储导致创建Multiton的计算。这个额外的层次解决了未使用实例的问题。

public class Multiton {

    private static final ConcurrentMap<Integer, Future<Multiton>> multitons = new ConcurrentHashMap<>();
    private static final Callable<Multiton> creator = new Callable<Multiton>() {
        public Multiton call() { return new Multiton(); }
    };

    private Multiton(Strnig key) {}

    public static Multiton getInstance(final Integer key) throws InterruptedException, ExecutionException {
        Future<Multiton> f = multitons.get(key);
        if (f == null) {
            FutureTask<Multiton> ft = new FutureTask<>(creator);
            f = multitons.putIfAbsent(key, ft);
            if (f == null) {
                f = ft;
                ft.run();
            }
        }
        return f.get();
    }
}

像这样!为什么要用 while (true) - OldCurmudgeon
1
@OldCurmudgeon 是的,它会等待直到调用run方法。 - assylias
使用这种记忆化方法,是否可以向多例构造函数添加参数?例如,nameOfCreator,genderOfCreator。我想不出任何办法,因为“Callable.call()”具有空参数列表。 - mike
1
@mike 假设参数已传递给 getInstance,则可以即时创建可调用对象来返回 new Multiton(params) 而不是标准的 creator。我也认为如果相同的键以不同的参数被调用,可能会导致一些混淆... - assylias
2
如果两个线程调用run(),FutureTask的行为是什么?我假设任务只运行一次,但是javadocs在这一点上并不完全清楚。编辑:我现在意识到,在这里不会发生这种情况,因为run()只由成功put() FutureTask的线程调用。 - Bryan Rink
显示剩余5条评论

6
这将为您的Multitons提供线程安全的存储机制。唯一的缺点是可能会创建一个Multiton,在putIfAbsent()调用中不会被使用。虽然可能性很小,但确实存在。当然,如果出现这种情况,仍然不会造成任何伤害。
另一方面,没有预分配或初始化要求,也没有预定义的大小限制。
private static ConcurrentHashMap<Integer, Multiton> instances = new ConcurrentHashMap<Integer, Multiton>();

public static Multiton getInstance(int which) 
{
    Multiton result = instances.get(which);

    if (result == null) 
    {
        Multiton m = new Multiton(...);
        result = instances.putIfAbsent(which, m);

        if (result == null)
            result = m;
    }

    return result;
}

如果键没有映射(请参阅文档),则putIfAbsent返回null。因此,第一次调用getInstance时,result仍将为null - sp00m
@Robin 在单例模式中,Obj引用被定义为volatile。如何使这些对象变得volatile - Pankaj Singhal

4
您可以使用锁数组,以便至少能够同时获取不同的实例:
private static final Multiton[] instances = new Multiton[...];
private static final Object[] locks = new Object[instances.length];

static {
    for (int i = 0; i < locks.length; i++) {
        locks[i] = new Object();
    }
}

private Multiton(...) {
    //...
}

public static Multiton getInstance(int which) {
    synchronized(locks[which]) {
        if(instances[which] == null) {
            instances[which] = new Multiton(...);
        }
        return instances[which];
    }
}

请注意,由于易失性数组语义,编写此代码的非阻塞版本要困难得多。 - Voo
这将允许您同时获取不同的实例,但是否有任何解决个别实例周围不必要同步的方法?在这一点上,似乎我们又回到了双重检查锁定困境。 - donnyton
2
最简单的解决方法是急切地初始化所有实例。这是最简单的解决方案,在许多情况下也是最好的解决方案。如果懒惰是绝对要求,首先确保简单同步构成性能问题,然后再尝试优化它。同步速度很快,或者至少比IO、数据库访问、进程间调用、O(n^2)算法等要快得多。不要过早优化,这是万恶之源。 - JB Nizet
在单例模式中,Obj引用被定义为“volatile”。如何使这些对象成为“volatile”? - Pankaj Singhal

3

随着Java 8的到来以及ConcurrentMap和lambda表达式的一些改进,现在可以以更加整洁的方式实现Multiton(甚至可能是Singleton):

public class Multiton {
  // Map from the index to the item.
  private static final ConcurrentMap<Integer, Multiton> multitons = new ConcurrentHashMap<>();

  private Multiton() {
    // Possibly heavy construction.
  }

  // Get the instance associated with the specified key.
  public static Multiton getInstance(final Integer key) throws InterruptedException, ExecutionException {
    // Already made?
    Multiton m = multitons.get(key);
    if (m == null) {
      // Put it in - only create if still necessary.
      m = multitons.computeIfAbsent(key, k -> new Multiton());
    }
    return m;
  }
}

我怀疑 - 虽然这会让我感到不舒服 - getInstance 可以进一步简化为:

// Get the instance associated with the specified key.
public static Multiton getInstance(final Integer key) throws InterruptedException, ExecutionException {
  // Put it in - only create if still necessary.
  return multitons.computeIfAbsent(key, k -> new Multiton());
}

为什么这让你感到不舒服? - assylias
@assylias - 因为我还没有完全理解将参数传递给只有可能被执行的方法的概念。那个 new Multiton() 看着我,我的习惯告诉我 正在创建一个对象。我知道这是不合理的 - 我只需要处理它。 - OldCurmudgeon
在单例模式中,Obj引用被定义为“volatile”。如何使这些对象成为“volatile”? - Pankaj Singhal

2
你正在寻找一个 AtomicReferenceArray
public class Multiton {
  private static final AtomicReferenceArray<Multiton> instances = new AtomicReferenceArray<Multiton>(1000);

  private Multiton() {
  }

  public static Multiton getInstance(int which) {
    // One there already?
    Multiton it = instances.get(which);
    if (it == null) {
      // Lazy make.
      Multiton newIt = new Multiton();
      // Successful put?
      if ( instances.compareAndSet(which, null, newIt) ) {
        // Yes!
        it = newIt;
      } else {
        // One appeared as if by magic (another thread got there first).
        it = instances.get(which);
      }
    }

    return it;
  }
}

这个解决方案的问题在于它可能会为同一个键调用Multiton的构造函数多次。如果执行else块,则newIt将被丢弃,因为另一个线程也创建了该对象。 - Bryan Rink
1
@BryanRink - 你说得完全正确。这里有一个更好的真正的Multiton并且是线程安全的,还有一个Java 8版本的在这里 - OldCurmudgeon

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