потоки, обращающиеся к несинхронизированным методам в Java

могу я попросить объяснить мне, как потоки и синхронизация работают в Java?

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

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

Если я могу гарантировать (или хотя бы попытаться гарантировать), что мои потоки никогда не изменяют данные, могу ли я использовать их для вызова несинхронизированных методов объектов, содержащих данные?

Если несколько потоков обращаются к несинхронизированному методу, который не изменяет никаких полей класса, но имеет некоторые внутренние переменные, безопасно ли это?

искусственный пример:

public class Data{
// this hash map is filled before I start threads
protected Map<Integer, Spike> allSpikes = new HashMap<Integer, Spike>();

public HashMap returnBigSpikes(){
     Map<Integer, Spike> bigSpikes = new HashMap<Integer, Spike>();

     for (Integer i: allSpikes.keySet()){
         if (allSpikes.get(i).spikeSize > 100){
         bigSpikes.put(i,allSpikes.get(i));
         }
     }

     return bigSpikes;
}
}

Безопасно ли вызывать несинхронизированный метод returnBigSpikes() из потоков?

Теперь я понимаю, что такие варианты использования потенциально очень опасны, потому что трудно контролировать, что данные (например, возвращенные bigSpikes) не будут изменены. Но я уже реализовал и протестировал его таким образом и хочу знать, могу ли я использовать результаты своего приложения сейчас и изменить архитектуру позже...

Что произойдет, если я сделаю методы синхронизированными? Будет ли приложение замедлено до 1 производительности процессора? Если да, то как я могу правильно его спроектировать и сохранить производительность?

(Я читаю около 20-40 Гб данных (сообщения журнала) в основную память, а затем запускаю потоки, которым нужно просмотреть все данные, чтобы найти в них какую-то корреляцию; каждый поток становится только частью сообщений для анализа; но для анализа поток должен сравнивать каждое сообщение от своей части с множеством других сообщений от данных, поэтому я сначала решил разрешить потокам читать данные без синхронизации).

Заранее большое спасибо.


person Andrey Sapegin    schedule 21.03.2013    source источник
comment
к returnBigSpikes обращаются несколько потоков одновременно?   -  person Vishal K    schedule 21.03.2013
comment
да. это не настоящий пример, но мое приложение уже работает так.   -  person Andrey Sapegin    schedule 21.03.2013
comment
Я думаю, что ваш код будет работать безопасно без использования синхронизации.   -  person Vishal K    schedule 21.03.2013


Ответы (6)


Если allSpikes заполнен до запуска всех потоков, вы можете убедиться, что он не будет изменен позже, сохранив его как неизменяемая карта.

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

person Keppil    schedule 21.03.2013
comment
+1 за упоминание о том, что Spike должен быть неизменным/поточно-ориентированным. Это потенциальная проблема здесь. - person Bruno Grieder; 21.03.2013

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

person Rich    schedule 21.03.2013
comment
В общем, если у вас есть куча потоков, где вы можете гарантировать, что только один поток будет изменять ресурс, а остальные будут только читать этот ресурс, тогда доступ к этому ресурсу не нужно синхронизировать - не совсем неправильно. Эта ситуация может вызвать состояние гонки. Если вы имеете в виду, что запись происходит первой (до любого другого потока), тогда ВСЕ чтения происходят ПОСЛЕ любой записи, тогда все в порядке. - person xagyg; 22.03.2013

Пока что-то практически неизменное (например, с использованием ключевого слова final) и вы используете немодифицируемую карту, все в порядке.

Я бы предложил следующие неизменяемые данные:

public class UnmodifiableData {
     final Map<Integer,Spike>  bigSpikes;
     public UnmodifiableData(Map<Integer,Spike> bigSpikes) {
         this.bigSpikes = Collections.unmodifiableMap(new HashMap<>(bigSpikes));
     }
     ....

}

person Chrstian Beutenmueller    schedule 22.03.2013

Ваш план должен работать нормально. Вам не нужно synchronize читать, только писать.

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

person OldCurmudgeon    schedule 21.03.2013
comment
это был не реальный пример... Однако, даже если все потоки получают одну и ту же карту, но все равно не модифицируют ее, это все равно безопасно, верно? - person Andrey Sapegin; 21.03.2013
comment
Да, это безопасно. Сложность возникает, если вы каким-то образом кэшируете подкарты (например, bigSpikes в вашем образце). Вы должны сделать кеши volatile и подумать, что происходит, когда два потока начинают создавать кэшированную карту одновременно. Если вы создадите все подкарты перед запуском потоков, то это, опять же, не проблема. - person OldCurmudgeon; 21.03.2013

Если вы используете ConcurrentHashMap, он сделает всю работу по синхронизации за вас. Это лучше, чем делать синхронизацию вокруг обычного HashMap.

person Mikhail    schedule 21.03.2013
comment
да, но я мог бы иметь и другие структуры. И мой вопрос более общий, я хочу также знать, смогу ли я использовать такой дизайн в будущем. Не будет ли ConcurrentHashMap замедлять работу моего приложения во время чтения/получения? - person Andrey Sapegin; 21.03.2013
comment
На практике это будет работать без сбоев, если вы читаете и не изменяете данные. Теоретически вы нарушаете правило модели памяти Java, что является плохой практикой. Если вам не нравится CuncurrentHashMap, взгляните на ReentrantReadWriteLock. - person Mikhail; 21.03.2013

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

person Evgeniy Dorofeev    schedule 21.03.2013
comment
а как насчет переменных внутри несинхронизированного метода? Например, «Map‹Integer, Spike› bigSpikes» и «Integer i». Будет ли каждый поток вызывать свой собственный экземпляр метода? - person Andrey Sapegin; 21.03.2013