Параллельные блокировки Java на уровне ключа карты

Есть писатель, который обновляет цены, вызывая метод putPrice. Читатель использует getPrice, чтобы получить последнюю цену. hasChangedMethod возвращает логическое значение, определяющее, изменилась ли цена с момента последнего вызова getPrice.

Я ищу самое быстрое решение. Я пытаюсь добиться потокобезопасного последовательного чтения/записи в карту на ключевом уровне.

Я думаю, что блокировка всей карты может вызвать проблемы с производительностью, поэтому я решил сделать это на ключевом уровне. К сожалению, это не работает должным образом и блокирует всю карту. Почему? Не могли бы вы помочь мне понять, что я делаю неправильно здесь?

ОБНОВЛЕНИЕ:

Думаю, мы можем подытожить двумя вопросами: 1. как мне предоставить свободный доступ к остальным ключам, если один из них находится в процессе обновления. 2. Как я могу гарантировать атомарные операции моих методов, поскольку они требуют нескольких операций чтения/записи. например getPrice() - получить цену и обновить флаг hasChanged.

PriceHolder.java

public final class PriceHolder {

    private ConcurrentMap<String, Price> prices;

    public PriceHolder() {
        this.prices = new ConcurrentHashMap<>();

        //Receive starting prices..
        Price EUR = new Price();
        EUR.setHasChangedSinceLastRead(true);
        EUR.setPrice(new BigDecimal(0));

        Price USD = new Price();
        USD.setHasChangedSinceLastRead(true);
        USD.setPrice(new BigDecimal(0));
        this.prices.put("EUR", EUR);
        this.prices.put("USD", USD);

    }

    /** Called when a price ‘p’ is received for an entity ‘e’ */
    public void putPrice(
            String e,
            BigDecimal p) throws InterruptedException {

            synchronized (prices.get(e)) {
                Price currentPrice = prices.get(e);
                if (currentPrice != null && !currentPrice.getPrice().equals(p)) {
                    currentPrice.setHasChangedSinceLastRead(true);
                    currentPrice.setPrice(p);
                } else {
                    Price newPrice = new Price();
                    newPrice.setHasChangedSinceLastRead(true);
                    newPrice.setPrice(p);
                    prices.put(e, newPrice);
                }
            }
    }

    /** Called to get the latest price for entity ‘e’ */
    public BigDecimal getPrice(String e) {
        Price currentPrice = prices.get(e);
        if(currentPrice != null){
            synchronized (prices.get(e)){
                currentPrice.setHasChangedSinceLastRead(false);
                prices.put(e, currentPrice);
            }
            return currentPrice.getPrice();
        }
        return null;
    }

    /**
     * Called to determine if the price for entity ‘e’ has
     * changed since the last call to getPrice(e).
     */
    public boolean hasPriceChanged(String e) {
        synchronized (prices.get(e)){
            return prices.get(e) != null ? prices.get(e).isHasChangedSinceLastRead() : false;
        }
    }
}

Цена.java

public class Price {

    private BigDecimal price;

    public boolean isHasChangedSinceLastRead() {
        return hasChangedSinceLastRead;
    }

    public void setHasChangedSinceLastRead(boolean hasChangedSinceLastRead) {
        this.hasChangedSinceLastRead = hasChangedSinceLastRead;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    private boolean hasChangedSinceLastRead = false;

}

person Wild Goat    schedule 03.11.2015    source источник
comment
Знаете ли вы заранее, какие ключи (я имею в виду названия валют, такие как EUR и USD) будут во время разработки, или автор также может добавить некоторые новые валюты во время выполнения?   -  person izce    schedule 03.11.2015
comment
@IzCe, да, я знаю все ключи заранее. В основном количество валют в мире. Может быть, я немного запутался, тогда как мне заблокировать его на ключевом уровне?   -  person Wild Goat    schedule 03.11.2015
comment
Поскольку вы заранее знаете ключи, вам не нужно использовать ConcurrentMap. Просто простой карты будет достаточно. При инициализации вашей программы основной поток может заполнить карту этими ключами и объектами Price со значениями по умолчанию. Все, что вам нужно, это получить объект Price и синхронизировать его для каждой операции get и put. Вам не нужно использовать вызов Thread.sleep(3000); в синхронизированном блоке, так как нет смысла ждать потока на таком критическом пути.   -  person izce    schedule 03.11.2015
comment
stackoverflow .com/questions/25998536/ Я не думаю, что вам нужно использовать синхронизированные блоки, поскольку ConcurrentHashMap уже гарантирует, что несколько потоков могут писать и читать. Блокировки реализуются на уровне сегмента хэша, а не на всей таблице.   -  person Shashank    schedule 03.11.2015
comment
И вам не нужна эта строка prices.put(e, currentPrice); в методе getPrice(...), поскольку у вас уже есть ссылка на тот же объект Price в currentPricevariable.   -  person izce    schedule 03.11.2015
comment
@Wild Goat: Должен ли поток авторов обновлять несколько валют одновременно? Аналогично, должен ли читатель потока читать сразу несколько валют?   -  person izce    schedule 03.11.2015
comment
@IzCe, да, писатель может обновлять несколько валют параллельно, а читатель может читать несколько валют одновременно. Как изменится дизайн, если я не буду знать ключ заранее?   -  person Wild Goat    schedule 03.11.2015
comment
Когда вы говорите, что блокирует всю карту, что именно вы имеете в виду? Похоже, вы удерживаете блокировку объекта цены в течение трех секунд, что может помешать чтению при частых операциях записи. Может ли это быть причиной проблемы, которую вы видите?   -  person Warren Dew    schedule 03.11.2015
comment
@Shashank, а как насчет логики в getPrice(), где я обновляю свойство hasChangedSilnceLastRead? Например, если поток 1 прочитал цену A, поток 2 изменил цену A, а затем я снова переопределяю последний поток 1 - несоответствие?   -  person Wild Goat    schedule 03.11.2015
comment
@WarrenDew, извините, thread.sleep() - это просто фиктивный код, который я использовал для тестирования. игнорируй это. Я хочу заблокировать чтение/запись на ключевом уровне, кажется, что ConcurrentMap может предложить мне это преимущество, но как мне справиться с входом в систему в getPrice(), где я выполняю несколько операций: чтение и обновление, может ли это вызвать несогласованность, если другой поток будет писать в между этими операциями?   -  person Wild Goat    schedule 03.11.2015
comment
synchronized (prices.get(e)) { - Что делать, если карта изначально пуста, т.е. get() возвращает null?   -  person JimmyB    schedule 03.11.2015
comment
@HannoBinder, да, вы правы, это выдаст нулевой указатель, опять же, если я проверю, прежде чем потеряю атомарность?   -  person Wild Goat    schedule 03.11.2015
comment
Ваш вопрос не ясен. Я не понимаю, почему вся карта будет заблокирована. Однако я бы посоветовал вам взглянуть на docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/. Он может делать то, что вы хотите: позволить читателям продвигаться вперед, в то время как потенциально медленные писатели борются за блокировку. Тем не менее, вы все равно должны держать критическую секцию для написания небольшой.   -  person Alexander Torstling    schedule 03.11.2015
comment
I think that locking the whole map may cause a performance issue, мыслить хорошо, но в таких случаях этого недостаточно. Вам также необходимо получить данные.   -  person biziclop    schedule 03.11.2015
comment
Еще один совет: не читайте карту несколько раз и не ждите каждый раз одного и того же результата, если вы пишете многопоточный код.   -  person Alexander Torstling    schedule 03.11.2015


Ответы (7)


Использование ConcurrentMap сильно зависит от версии Java. При использовании Java 8 или более поздней версии вы получаете почти все бесплатно:

public final class PriceHolder {

    private ConcurrentMap<String, Price> prices;

    public PriceHolder() {
        this.prices = new ConcurrentHashMap<>();

        //Receive starting prices..
        Price EUR = new Price();
        EUR.setHasChangedSinceLastRead(true);
        EUR.setPrice(BigDecimal.ZERO);

        Price USD = new Price();
        USD.setHasChangedSinceLastRead(true);
        USD.setPrice(BigDecimal.ZERO);
        this.prices.put("EUR", EUR);
        this.prices.put("USD", USD);

    }

    /** Called when a price ‘p’ is received for an entity ‘e’ */
    public void putPrice(String e, BigDecimal p) {
        prices.compute(e, (k, price)-> {
            if(price==null) price=new Price();
            price.setHasChangedSinceLastRead(true);
            price.setPrice(p);
            return price;
        });
    }

    /** Called to get the latest price for entity ‘e’ */
    public BigDecimal getPrice(String e) {
        Price price = prices.computeIfPresent(e, (key, value) -> {
            value.setHasChangedSinceLastRead(false);
            return value;
        });
        return price==null? null: price.getPrice();
    }

    /**
     * Called to determine if the price for entity ‘e’ has
     * changed since the last call to getPrice(e).
     */
    public boolean hasPriceChanged(String e) {
        final Price price = prices.get(e);
        return price!=null && price.isHasChangedSinceLastRead();
    }
}

Методы compute… на параллельной карте блокируют затронутую запись на время вычисления, позволяя продолжить обновление всех других записей. Для простого доступа get, как в hasPriceChanged, дополнительная синхронизация не требуется, если вы вызываете ее только один раз в методе, т.е. сохраняете результат в локальной переменной во время проверки.


До появления Java 8 все было сложнее. Там все предложения ConcurrentMap представляют собой определенные атомарные методы обновления, которые можно использовать для создания более высокоуровневых методов обновления методом проб и повторений.

Чтобы использовать его аккуратно, лучше всего сделать класс значения неизменяемым:

public final class Price {

    private final BigDecimal price;
    private final boolean hasChangedSinceLastRead;

    Price(BigDecimal value, boolean changed) {
      price=value;
      hasChangedSinceLastRead=changed;
    }
    public boolean isHasChangedSinceLastRead() {
        return hasChangedSinceLastRead;
    }
    public BigDecimal getPrice() {
        return price;
    }
}

Затем используйте его, чтобы всегда создавать новый объект, отражающий желаемое новое состояние, и выполнять атомарные обновления, используя либо putIfAbsent, либо replace:

public final class PriceHolder {

    private ConcurrentMap<String, Price> prices;

    public PriceHolder() {
        this.prices = new ConcurrentHashMap<>();

        //Receive starting prices..
        Price EUR = new Price(BigDecimal.ZERO, true);
        Price USD = EUR; // we can re-use immutable objects...
        this.prices.put("EUR", EUR);
        this.prices.put("USD", USD);
    }

    /** Called when a price ‘p’ is received for an entity ‘e’ */
    public void putPrice(String e, BigDecimal p) {
      Price old, _new=new Price(p, true);
      do old=prices.get(e);
      while(old==null? prices.putIfAbsent(e,_new)!=null: !prices.replace(e,old,_new));
    }

    /** Called to get the latest price for entity ‘e’ */
    public BigDecimal getPrice(String e) {
        for(;;) {
          Price price = prices.get(e);
          if(price==null) return null;
          if(!price.isHasChangedSinceLastRead()
          || prices.replace(e, price, new Price(price.getPrice(), false)))
            return price.getPrice();
        }
    }

    /**
     * Called to determine if the price for entity ‘e’ has
     * changed since the last call to getPrice(e).
     */
    public boolean hasPriceChanged(String e) {
        final Price price = prices.get(e);
        return price!=null && price.isHasChangedSinceLastRead();
    }
}
person Holger    schedule 03.11.2015
comment
Имейте в виду, что вычисления требуют производительности записи (60 млн операций в секунду против 1 млрд операций в секунду). Вы, вероятно, захотите использовать схему стиля блокировки с двойной проверкой, чтобы оптимистично иметь производительность чтения в общем случае. - person Ben Manes; 03.11.2015
comment
@Ben Manes: Compute является записью здесь. Даже если он возвращает существующий объект, он изменит два свойства. Таким образом, без семантики записи потребуется дополнительная блокировка. Оптимистическое чтение было бы полезно, если вы предполагаете, что более чем редко случается, что конкретная операция не требует выполнения обновления, но это не дается вопросом. - person Holger; 03.11.2015
comment
Извините, я имел в виду вычисления в общем смысле. В частности, я имел в виду использование getPrice computeIfPresent. Поскольку суть проблемы заключалась в проблеме с производительностью, казалось, что это стоит указать. - person Ben Manes; 03.11.2015
comment
@Ben Manes: но даже getPrice включает запись. Но вы правы, в зависимости от фактического варианта использования есть альтернативы. Однако вопрос в том, чтобы избежать блокировки всей карты, и это очень простой способ решения этой проблемы, который я бы усложнил только в том случае, если результирующая производительность действительно является проблемой… - person Holger; 03.11.2015

Как насчет чего-то вроде

class AtomicPriceHolder {

  private volatile BigDecimal value;
  private volatile boolean dirtyFlag;

  public AtomicPriceHolder( BigDecimal initialValue) {
    this.value = initialValue;
    this.dirtyFlag = true;
  }

  public synchronized void updatePrice( BigDecimal newPrice ) {
    if ( this.value.equals( newPrice ) == false) {
      this.value = newPrice;
      this.dirtyFlag = true;
    }
  }

  public boolean isDirty() {
    return this.dirtyFlag;
  }

  public BigDecimal peek() {
    return this.value;
  }

  public synchronized BigDecimal read() {
    this.dirtyFlag = false;
    return this.value;
  }

}

...

public void updatePrice( String id, BigDecimal value ) {

  AtomicPriceHolder holder;
  synchronized( someGlobalSyncObject ) {
    holder = prices.get(id);
    if ( holder == null ) {
      prices.put( id, new AtomicPriceHolder( value ) );
      return;
    }
  }

  holder.updatePrice( value );

}

Однако обратите внимание, что это, вероятно, не имеет никакого смысла, потому что фактическая атомарная модификация значения цены происходит настолько быстро, что вы не можете ожидать никакой выгоды от разблокировки карты раньше.

Условные операции «проверить, есть ли это на карте, создать новую и вставить, если нет» должны быть атомарными и должны выполняться путем блокировки всей карты на этот короткий период. Все остальное потребует отдельного объекта синхронизации для каждого ключа. Их нужно было бы где-то хранить и управлять ими, а доступ к этому хранилищу нужно было бы снова синхронизировать и т. д.

Просто сделайте грубую блокировку, чтобы убедиться, что у вас есть правильность, а затем двигайтесь дальше.

person JimmyB    schedule 03.11.2015

  1. как предоставить свободный доступ к остальным ключам, если один из них находится в процессе обновления.

Простого использования ConcurrentHashMap достаточно, чтобы обеспечить свободный доступ к ключам; get не создают конфликтов, а put блокируют только подмножество ключей, а не всю карту.

  1. Как я могу гарантировать атомарные операции моих методов, поскольку они требуют нескольких операций чтения/записи.

Чтобы обеспечить согласованность, вам необходимо синхронизировать какой-либо общий объект (или использовать другой механизм блокировки, например ReentrantLock); Я бы предложил создать ConcurrentHashMap<String, Object> объектов блокировки, чтобы вы могли:

synchronized (locks.get(e)) { ... }

Просто заполните карту new Object(). Риск используемого вами паттерна (блокировка объектов Price) заключается в том, что теперь эти объекты должны сохраняться и никогда не заменяться. Легче обеспечить это, имея выделенную частную коллекцию блокировок, а не перегружая свой тип значения в качестве механизма блокировки.


Кроме того, если вы пытаетесь выполнять денежные операции на Java, вам обязательно следует использовать Joda- Money вместо того, чтобы изобретать велосипед.

person dimo414    schedule 03.11.2015

hasChangedMethod возвращает логическое значение, определяющее, изменилась ли цена с момента последнего вызова getPrice.

Это проблематичный шаблон, поскольку hasPriceChanged по существу должен возвращать что-то другое для каждого потока. Если вы можете немного подробнее рассказать о том, что вы на самом деле пытаетесь сделать (т.е. почему вы думаете, что вам нужен этот шаблон), возможно, вы сможете предложить альтернативу. Например, можно полностью отказаться от hasPriceChanged и просто рассматривать эту структуру данных как каноническую и каждый раз запрашивать ее текущее значение.


Тем не менее, вот как я могу реализовать поведение, которое вы ищете. Вполне могут быть альтернативы, это только первый проход.

Держите ConcurrentHashMap<String, ThreadLocal<Boolean>>; ThreadLocal будет хранить статус вызовов get для каждого потока. Я также использую отдельную приватную карту замков.

ConcurrentHashMap<String, Price> pricesMap;
ConcurrentHashMap<String, ThreadLocal<Boolean>> seenMap;
ConcurrentHashMap<String, Object> lockMap;

private Object getLock(String key) {
  return lockMap.computeIfAbsent(key, k -> new Object());
}

private ThreadLocal<Boolean> getSeen(String key) {
  return seenMap.computeIfAbsent(e,
      ThreadLocal.<Boolean>withInitial(() -> false));
}

public void putPrice(String e, BigDecimal p) {
  synchronized (getLock(e)) {
    // price has changed, clear the old state to mark all threads unseen
    seenMap.remove(e);
    pricesMap.get(e).setPrice(p);
  }
}

public BigDecimal getPrice(String e) {
  synchronized (getLock(e)) {
    // marks the price seen for this thread
    getSeen(e).set(true);
    BigDecimal price = pricesMap.get(e);
    return price != null ? price.getPrice() : null;
  }
}

public boolean hasPriceChanged(String e) {
  synchronized (getLock(e)) {
    return !getSeen(e).get();
  }
}

Обратите внимание, что несмотря на то, что структура данных является потокобезопасной, здесь все еще существует риск состояния гонки — вы можете вызвать hasPriceChanged() и получить обратно false, сразу после чего цена будет изменена другим потоком. Отказ от этого hasPriceChanged() поведения, скорее всего, упростит ваш код.

person dimo414    schedule 03.11.2015
comment
Отлично, позвольте мне попробовать! Кстати, есть некоторые проблемы с типом, возвращаемым лямбдой в методе getSeen(). - person Wild Goat; 03.11.2015
comment
Извините за это, я просто закодировал это внутри, поэтому могут быть небольшие опечатки. Вы всегда можете написать его в стиле Java 7, если это необходимо. Не стесняйтесь редактировать этот пост со своими изменениями. - person dimo414; 03.11.2015

Я бы рекомендовал использовать наблюдаемый шаблон наблюдателя. Не нужно заново изобретать велосипед. См. Наблюдатель и Наблюдаемый

Я бы также рекомендовал изучить условие Так как нет необходимости запирать полный объект для всех читателей. Чтение может быть одновременным, а письмо — нет.

Если коллекция параллельна, это не означает, что она волшебным образом синхронизирует все. Это просто гарантирует, что их методы потокобезопасны. Как только вы покидаете область действия функции, блокировка снимается. Поскольку вам нужен более продвинутый способ управления синхронизацией, лучше всего взять это в свои руки и использовать обычный HashMap.

Некоторые примечания:

Вы злоупотребляете HashMap.get. Подумайте о том, чтобы получить его один раз и сохранить в переменной.

synchronized (prices.get(e))

Это может вернуть null, и вы должны проверить это. синхронизация на нулевых объектах не допускается.

prices.put(e, currentPrice);

Я не уверен, что это задумано, но это действие не нужно. См. это

person Neijwiert    schedule 03.11.2015
comment
Это может вернуть null, и вы должны проверить это. -- Может оказаться сложнее, чем кажется. Вы не можете атомарно проверить, является ли объект нулевым, и синхронизировать его. - person JimmyB; 04.11.2015

Лучший способ получить то, что вы хотите, — это, вероятно, пойти дальше и использовать ConcurrentMap для карты, а всю остальную синхронизацию поместить в ваш класс Price. Это приведет к более простому коду, который всегда очень ценен в многопоточной среде, чтобы избежать незначительных ошибок, а также достичь ваших целей одновременного доступа к карте и отслеживания того, была ли запись с момента последнего чтения для каждой валюты.

В классе Price всякий раз, когда вы устанавливаете цену, вы также хотите установить hasChangedSinceLastRead; эти две вещи идут вместе как одна операция, которая должна быть атомарной. Всякий раз, когда вы читаете цену, вы также хотите очистить hasChangedSinceLastRead; это вы тоже хотите быть атомарным. Таким образом, класс должен разрешать модифицировать hasChangedSinceLastRead только этим двум операциям, а не оставлять логику другим классам, а методы должны быть синхронизированы, чтобы гарантировать, что price и hasChangedSinceLastRead не могут выйти из синхронизации из-за доступа нескольких потоков. Теперь класс должен выглядеть так:

public class Price {

    public boolean isHasChangedSinceLastRead() {
        return hasChangedSinceLastRead;
    }

    // setHasChangedSinceLastRead() removed

    public synchronized BigDecimal getPrice() {
        hasChangedSinceLastRead = false;
        return price;
    }

    public synchronized void setPrice(BigDecimal newPrice) {
        if (null != price && price.equals(newPrice) {
            return;
        }
        price = newPrice;
        hasChangedSinceLastRead = true;
    }

    private BigDecimal price;
    private volatile boolean hasChangedSinceLastRead = false;
}

Обратите внимание, что вы можете либо сделать isHasChangedSinceLastRead() синхронизированным, либо hasChangedSinceLastRead volatile; Я выбрал последнее, оставив isHasChangedSinceLastRead() несинхронизированным, потому что для синхронизации метода требуется полный барьер памяти, а для создания переменной volatile требуется только считываемая половина барьера памяти при чтении переменной.

Причина, по которой чтение hasChangedSinceLastRead требует некоторого барьера памяти, заключается в том, что синхронизированные методы, блоки и доступ к энергозависимой памяти гарантируют только эффективный порядок выполнения — отношение «происходит до» — с другими синхронизированными методами, блоками и доступом к энергозависимой памяти. Если isHasChangedSinceLastRead не синхронизирована и переменная не является изменчивой, отношения «произошло до» не существует; в этом случае isHasChangedSinceLastRead может вернуть "true", но вызывающий его поток может не увидеть изменение цены. Это связано с тем, что установка price и hasChangedSinceLastRead в setPrice() может быть воспринята другими потоками в обратном порядке, если отношение "происходит раньше" не установлено.

Теперь, когда вся необходимая синхронизация находится в классах ConcurrentMap и Price, вам больше не нужно выполнять какую-либо синхронизацию в PriceHolder, а PriceHolder больше не нужно беспокоиться об обновлении hasChangedSinceLastRead. Код упрощен до:

public final class PriceHolder {

    private ConcurrentMap<String, Price> prices;

    public PriceHolder() {
        prices = new ConcurrentHashMap<>();

        //Receive starting prices..
        Price EUR = new Price();
        EUR.setPrice(new BigDecimal(0));
        this.prices.put("EUR", EUR);

        Price USD = new Price();
        USD.setPrice(new BigDecimal(0));
        this.prices.put("USD", USD);
    }

    /** Called when a price ‘p’ is received for an entity ‘e’ */
    public void putPrice(
        String e,
        BigDecimal p
    ) throws InterruptedException {
        Price currentPrice = prices.get(e);
        if (null == currentPrice) {
            currentPrice = new Price();
            currentPrice = prices.putIfAbsent(e);
        }
        currentPrice.setPrice(p);
    }

    /** Called to get the latest price for entity ‘e’ */
    public BigDecimal getPrice(String e) {
        Price currentPrice = prices.get(e);
        if (currentPrice != null){
            return currentPrice.getPrice();
        }
        return null;
    }

    /**
     * Called to determine if the price for entity ‘e’ has
     * changed since the last call to getPrice(e).
     */
    public boolean hasPriceChanged(String e) {
        Price currentPrice = prices.get(e);
        return null != currentPrice ? currentPrice.isHasChangedSinceLastRead() : false;
    }
}
person Warren Dew    schedule 04.11.2015
comment
Ух ты! Очень просто и чисто. Не могли бы вы еще раз объяснить, зачем нам volatile на hasChangedSinceLastRead - я думал, что если мы поставим synchronized на метод, он заблокирует весь объект. - person Wild Goat; 04.11.2015
comment
Добавлен абзац, объясняющий, почему либо isHasChangedSinceLastRead() должен быть синхронизирован, либо hasChangedSinceLastRead должен быть изменчивым. Достаточно одного, но нам нужен один из двух. Я выбрал последний, потому что у него меньше накладных расходов на выполнение. Подробнее в отредактированном ответе. - person Warren Dew; 04.11.2015

Я думаю, что вы можете заблокировать только свой конкретный ключ.

import java.util.HashMap;

public class CustomHashmap<K, V> extends HashMap<K, V>{

    private static final long serialVersionUID = 1L;

    @Override
    public V put(K key, V value) {
        V val = null;
        synchronized (key) {
            val = super.put(key, value);
        }

        return val;
    }

    @Override
    public V get(Object key) {
        return super.get(key);
    }


}
person Ankit Dixit    schedule 19.06.2018