Потокобезопасный многотонный шаблон
Вдохновленный комментарием к данному ответу, я попытался создать многопотоковую реализацию многотонного шаблона, ориентированного на многопотоковую обработку, который опирается на уникальные ключи и выполняет их блокировки (у меня есть идея из ответа Дж. Б. Низета на этот вопрос).
Вопрос
Является ли реализация, которую я предоставил, жизнеспособной?
Меня не интересует, являются ли Multiton (или Singleton) в целом хорошими моделями, это приведет к обсуждению. Я просто хочу чистую и работающую реализацию.
Противоположности:
- Вы должны знать, сколько экземпляров вы хотите создать во время компиляции.
Pros
- Нет блокировки на весь класс или всю карту. Параллельные звонки на
getInstance
возможны - Получение экземпляров через ключевой объект, а не только неограниченный
int
или жеString
, так что вы можете быть уверены, что получите не ненулевой экземпляр после вызова метода. - Потокобезопасен (по крайней мере, это мое впечатление).
public class Multiton
{
private static final Map<Enum<?>, Multiton> instances = new HashMap<Enum<?>, Multiton>();
private Multiton() {System.out.println("Created instance."); }
/* Can be called concurrently, since it only synchronizes on id */
public static <KEY extends Enum<?> & MultitionKey> Multiton getInstance(KEY id)
{
synchronized (id)
{
if (instances.get(id) == null)
instances.put(id, new Multiton());
}
System.out.println("Retrieved instance.");
return instances.get(id);
}
public interface MultitionKey { /* */ }
public static void main(String[] args) throws InterruptedException
{
//getInstance(Keys.KEY_1);
getInstance(OtherKeys.KEY_A);
Runnable r = new Runnable() {
@Override
public void run() { getInstance(Keys.KEY_1); }
};
int size = 100;
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < size; i++)
threads.add(new Thread(r));
for (Thread t : threads)
t.start();
for (Thread t : threads)
t.join();
}
enum Keys implements MultitionKey
{
KEY_1;
/* define more keys */
}
enum OtherKeys implements MultitionKey
{
KEY_A;
/* define more keys */
}
}
Я пытался предотвратить изменение размера карты и неправильное использование перечислений, на которых я синхронизируюсь. Это скорее подтверждение концепции, прежде чем я смогу покончить с этим!:)
public class Multiton
{
private static final Map<MultitionKey, Multiton> instances = new HashMap<MultitionKey, Multiton>((int) (Key.values().length/0.75f) + 1);
private static final Map<Key, MultitionKey> keyMap;
static
{
Map<Key, MultitionKey> map = new HashMap<Key, MultitionKey>();
map.put(Key.KEY_1, Keys.KEY_1);
map.put(Key.KEY_2, OtherKeys.KEY_A);
keyMap = Collections.unmodifiableMap(map);
}
public enum Key {
KEY_1, KEY_2;
}
private Multiton() {System.out.println("Created instance."); }
/* Can be called concurrently, since it only synchronizes on KEY */
public static <KEY extends Enum<?> & MultitionKey> Multiton getInstance(Key id)
{
@SuppressWarnings ("unchecked")
KEY key = (KEY) keyMap.get(id);
synchronized (keyMap.get(id))
{
if (instances.get(key) == null)
instances.put(key, new Multiton());
}
System.out.println("Retrieved instance.");
return instances.get(key);
}
private interface MultitionKey { /* */ }
private enum Keys implements MultitionKey
{
KEY_1;
/* define more keys */
}
private enum OtherKeys implements MultitionKey
{
KEY_A;
/* define more keys */
}
}
3 ответа
Это абсолютно не потокобезопасно. Вот простой пример из множества вещей, которые могут пойти не так.
Поток А пытается поставить на ключ id1
, Поток B изменяет размер таблицы ведра из-за id2
, Поскольку у них разные мониторы синхронизации, они параллельно участвуют в гонках.
Thread A Thread B
-------- --------
b = key.hash % map.buckets.size
copy map.buckets reference to local var
set map.buckets = new Bucket[newSize]
insert keys from old buckets into new buckets
insert into map.buckets[b]
В этом примере, скажем, Thread A
увидел map.buckets = new Bucket[newSize]
модификация. Это не гарантировано (так как не бывает прерванного до), но может. В этом случае он будет вставлять пару (ключ, значение) в неправильный сегмент. Никто никогда не найдет это.
В качестве небольшого варианта, если Thread A
скопировал map.buckets
ссылка на локальную переменную и выполнит всю свою работу над этим, тогда она будет вставлена в правильное ведро, но не в ту таблицу ведра; это не будет вставлять в новый, который Thread B
собирается установить в качестве таблицы для всех, чтобы увидеть. Если следующая операция на key 1
случается, увидит новую таблицу (опять же, не гарантируется, но может), тогда он не увидит Thread A's
действия, потому что они были сделаны в давно забытом массиве сегментов.
Я бы сказал, не жизнеспособным.
Синхронизация на id
параметр чреват опасностями - что делать, если они используют это enum
для другого механизма синхронизации? И конечно HashMap
не совпадает, как отмечалось в комментариях.
Чтобы продемонстрировать - попробуйте это:
Runnable r = new Runnable() {
@Override
public void run() {
// Added to demonstrate the problem.
synchronized(Keys.KEY_1) {
getInstance(Keys.KEY_1);
}
}
};
Вот реализация, которая использует атомику вместо синхронизации и, следовательно, должна быть более эффективной. Это намного сложнее, чем у вас, но обрабатывает все крайние случаи в Miltiton
Это сложно.
public class Multiton {
// The static instances.
private static final AtomicReferenceArray<Multiton> instances = new AtomicReferenceArray<>(1000);
// Ready for use - set to false while initialising.
private final AtomicBoolean ready = new AtomicBoolean();
// Everyone who is waiting for me to initialise.
private final Queue<Thread> waiters = new ConcurrentLinkedQueue<>();
// For logging (and a bit of linguistic fun).
private final int forInstance;
// We need a simple constructor.
private Multiton(int forInstance) {
this.forInstance = forInstance;
log(forInstance, "New");
}
// The expensive initialiser.
public void init() throws InterruptedException {
log(forInstance, "Init");
// ... presumably heavy stuff.
Thread.sleep(1000);
// We are now ready.
ready();
}
private void ready() {
log(forInstance, "Ready");
// I am now ready.
ready.getAndSet(true);
// Unpark everyone waiting for me.
for (Thread t : waiters) {
LockSupport.unpark(t);
}
}
// Get the instance for that one.
public static Multiton getInstance(int which) throws InterruptedException {
// One there already?
Multiton it = instances.get(which);
if (it == null) {
// Lazy make.
Multiton newIt = new Multiton(which);
// Successful put?
if (instances.compareAndSet(which, null, newIt)) {
// Yes!
it = newIt;
// Initialise it.
it.init();
} else {
// One appeared as if by magic (another thread got there first).
it = instances.get(which);
// Wait for it to finish initialisation.
// Put me in its queue of waiters.
it.waiters.add(Thread.currentThread());
log(which, "Parking");
while (!it.ready.get()) {
// Park me.
LockSupport.park();
}
// I'm not waiting any more.
it.waiters.remove(Thread.currentThread());
log(which, "Unparked");
}
}
return it;
}
// Some simple logging.
static void log(int which, String s) {
log(new Date(), "Thread " + Thread.currentThread().getId() + " for Multiton " + which + " " + s);
}
static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
// synchronized so I don't need to make the DateFormat ThreadLocal.
static synchronized void log(Date d, String s) {
System.out.println(dateFormat.format(d) + " " + s);
}
// The tester class.
static class MultitonTester implements Runnable {
int which;
private MultitonTester(int which) {
this.which = which;
}
@Override
public void run() {
try {
Multiton.log(which, "Waiting");
Multiton m = Multiton.getInstance(which);
Multiton.log(which, "Got");
} catch (InterruptedException ex) {
Multiton.log(which, "Interrupted");
}
}
}
public static void main(String[] args) throws InterruptedException {
int testers = 50;
int multitons = 50;
// Do a number of them. Makes n testers for each Multiton.
for (int i = 1; i < testers * multitons; i++) {
// Which one to create.
int which = i / testers;
//System.out.println("Requesting Multiton " + i);
new Thread(new MultitonTester(which+1)).start();
}
}
}
Я не программист на Java, но: HashMap
не является безопасным для одновременного доступа. Могу ли я рекомендовать ConcurrentHashMap
,
private static final ConcurrentHashMap<Object, Multiton> instances = new ConcurrentHashMap<Object, Multiton>();
public static <TYPE extends Object, KEY extends Enum<Keys> & MultitionKey<TYPE>> Multiton getInstance(KEY id)
{
Multiton result;
synchronized (id)
{
result = instances.get(id);
if(result == null)
{
result = new Multiton();
instances.put(id, result);
}
}
System.out.println("Retrieved instance.");
return result;
}