самый эффективный способ увеличить значение map в java

<p>я надеюсь, что этот вопрос не считается слишком основным для этого форума, но посмотрим. мне интересно, как реорганизовать некоторый код для повышения производительности, который запускается несколько раз.</p><p>скажем, я создаю список частот word, используя карту (возможно, hashmap), где каждый ключ представляет собой строку со счетчиком word, а значение представляет собой целое число, которое увеличивается каждый раз при обнаружении токена word.</p><p>в perl увеличение такого значения было бы несложно:</p><pre><code>$map{$word}++; </code></pre><p>но в java все гораздо сложнее. вот как я сейчас это делаю:</p><pre><code>int count = map.containskey(word) ? map.get(word) : 0; map.put(word, count + 1); </code></pre><p>что, конечно, зависит от функции автобокса в новых версиях java. интересно, можете ли вы предложить более эффективный способ увеличения такой стоимости? существуют ли даже хорошие причины производительности для отказа от среды collections и использования чего-то еще?</p><p>обновление: я проверил несколько ответов. увидеть ниже.</p>

<h2>некоторые результаты теста</h2><p>я получил много хороших ответов на этот вопрос - спасибо, ребята - поэтому я решил провести несколько тестов и выяснить, какой метод на самом деле самый быстрый. вот пять методов, которые я протестировал:</p><ul> <li>метод "containskey", который я представил в <a href="https://stackoverflow.com/questions/81346/most-efficient-way-to-increment-a-map-value-in-java">вопрос</a> </li> <li>метод "testfornull", предложенный александром димитровым</li> <li>метод "atomiclong", предложенный хэнком гей</li> <li>метод trove, предложенный джрудолом</li> <li>метод "mutableint", предложенный phax.myopenid.com</li> </ul><h2>метод</h2><p>вот что я сделал ...</p><ol> <li>создал пять классов, которые были идентичны, за исключением различий, показанных ниже. каждый класс должен был выполнить операцию, типичную для сценария, который я представил: открыть файл 10 мб и прочитать его, а затем выполнить подсчет частоты всех токенов word в файле. так как это заняло в среднем всего 3 секунды, мне пришлось выполнять подсчет частоты (не i/o) 10 раз.</li> <li>рассчитал цикл из 10 итераций, но <em>, а не операции ввода-вывода </em>, и записал общее время, затраченное (в секундах), по существу, используя <a href="http://books.google.com/books?id=t85jm-zwtx0c&printsec=frontcover&dq=java+cookbook&sig=acfu3u1lae1vnbvuwdicwetpaxzi1xvuxq#ppa734,m1">метод яна дарвина в java кулинарная книга</a> .</li> <li>выполнил все пять тестов в серии, а затем сделал это еще три раза.</li> <li>усреднил четыре результата для каждого метода.</li> </ol><h2>результаты</h2><p>сначала я представлю результаты и код ниже для тех, кто заинтересован.</p><p>как и ожидалось, метод <strong> containskey </strong> был самым медленным, поэтому я приведу скорость каждого метода по сравнению со скоростью этого метода.</p><ul> <li><strong> containskey: </strong> 30,654 секунды (базовый уровень)</li> <li><strong> atomiclong: </strong> 29,780 секунд (в 1,03 раза быстрее)</li> <li><strong> testfornull: </strong> 28.804 секунды (в 1,06 раза быстрее)</li> <li><strong> trove: </strong> 26,313 секунд (в 1,16 раза быстрее)</li> <li><strong> mutableint: </strong> 25,747 секунд (в 1,19 раза быстрее)</li> </ul><h2>выводы</h2><p>может показаться, что только метод mutableint и метод trove значительно быстрее, и только они дают прирост производительности более чем на 10%. однако, если многопоточность является проблемой, atomiclong может быть более привлекательным, чем другие (я не совсем уверен). я также запустил testfornull с переменными <code>final</code>, но разница была незначительной.</p><p>обратите внимание, что я не профилировал использование памяти в различных сценариях. я был бы рад услышать от любого, кто имеет хорошее представление о том, как методы mutableint и trove могут повлиять на использование памяти.</p><p>лично я считаю метод mutableint наиболее привлекательным, поскольку он не требует загрузки сторонних классов. так что, если я не обнаружу проблемы с этим, я, скорее всего, пойду.</p><h2>код</h2><p>вот ключевой код из каждого метода.</p><h3>containskey</h3><pre><code>import java.util.hashmap; import java.util.map; ... map<string, integer> freq = new hashmap<string, integer>(); ... int count = freq.containskey(word) ? freq.get(word) : 0; freq.put(word, count + 1); </code></pre><h3>testfornull</h3><pre><code>import java.util.hashmap; import java.util.map; ... map<string, integer> freq = new hashmap<string, integer>(); ... integer count = freq.get(word); if (count == null) { freq.put(word, 1); } else { freq.put(word, count + 1); } </code></pre><h3>atomiclong</h3><pre><code>import java.util.concurrent.concurrenthashmap; import java.util.concurrent.concurrentmap; import java.util.concurrent.atomic.atomiclong; ... final concurrentmap<string, atomiclong> map = new concurrenthashmap<string, atomiclong>(); ... map.putifabsent(word, new atomiclong(0)); map.get(word).incrementandget(); </code></pre><h3>trove</h3><pre><code>import gnu.trove.tobjectinthashmap; ... tobjectinthashmap<string> freq = new tobjectinthashmap<string>(); ... freq.adjustorputvalue(word, 1, 1); </code></pre><h3>mutableint</h3><pre><code>import java.util.hashmap; import java.util.map; ... class mutableint { int value = 1; // note that we start at 1 since we're counting public void increment () { ++value; } public int get () { return value; } } ... map<string, mutableint> freq = new hashmap<string, mutableint>(); ... mutableint count = freq.get(word); if (count == null) { freq.put(word, new mutableint()); } else { count.increment(); } </code></pre>

<p>хорошо, может быть старый вопрос, но есть более короткий путь с java 8:</p><pre><code>map.merge(key, 1, integer::sum) </code></pre><p>что он делает: если <em> ключ </em> не существует, укажите <em> 1 </em> в качестве значения в противном случае <em> сумма 1 </em> к значению, связанному с <em> ключом </em>. дополнительная информация <a href="https://docs.oracle.com/javase/8/docs/api/java/util/map.html#merge-k-v-java.util.function.bifunction-">здесь</a> </p>

<p>небольшое исследование в 2016 году: <a href="https://github.com/leventov/java-word-count">https://github.com/leventov/java-word-count</a> , <a href="https://github.com/leventov/java-word-count/blob/8a12a5ea4f53f01b9ee0e57f8aec70d75bc2030d/src/jmh/java/com/koloboke/wordcount/wordcount.java">исходный код теста</a> </p><p>лучшие результаты по методу (чем меньше, тем лучше):</p><pre><code> time, ms kolobokecompile 18.8 koloboke 19.8 trove 20.8 fastutil 22.7 mutableint 24.3 atomicinteger 25.3 eclipse 26.9 hashmap 28.0 hppc 33.6 hppcrt 36.5 </code></pre><p>время\пространство результаты: <img src="https://i.stack.imgur.com/nr5yp.png" width="600"></p>

<h2>google <a href="https://github.com/google/guava">гуава</a> твой друг ...</h2><p>... по крайней мере, в некоторых случаях. у них есть этот nice <a href="http://google.github.io/guava/releases/21.0/api/docs/index.html?com/google/common/util/concurrent/atomiclongmap.html"><em> atomiclongmap </em></a> . особенно приятно, потому что вы имеете дело с <em> long </em> как значением на вашей карте.</p><p>например.</p><pre class="lang-java prettyprint-override"><code>atomiclongmap<string> map = atomiclongmap.create(); [...] map.getandincrement(word); </code></pre><p>также возможно добавить более 1 к значению:</p><pre class="lang-java prettyprint-override"><code>map.getandadd(word, 112l); </code></pre>

<p>@ хэнк гей</p><p>как продолжение моего собственного (довольно бесполезного) комментария: троув выглядит как путь. если по какой-либо причине вы хотите придерживаться стандартного jdk, <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/concurrentmap.html">concurrentmap</a> и <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/atomic/atomiclong.html">atomiclong</a> может сделать код <em> крошечным </em> немного приятнее, хотя ymmv.</p><pre><code> final concurrentmap<string, atomiclong> map = new concurrenthashmap<string, atomiclong>(); map.putifabsent("foo", new atomiclong(0)); map.get("foo").incrementandget(); </code></pre><p>оставит <code>1</code> как значение на карте для <code>foo</code>. на самом деле, повышенный уровень дружелюбия к потокам - это все, что этот подход должен рекомендовать.</p>

<p>это всегда хорошая идея, чтобы посмотреть <a href="http://code.google.com/p/google-collections/">библиотека коллекций google</a> для такого рода вещей. в этом случае <a href="http://google-collections.googlecode.com/svn/trunk/javadoc/com/google/common/collect/multiset.html">multiset</a> добьется цели:</p><pre><code>multiset bag = multisets.newhashmultiset(); string word = "foo"; bag.add(word); bag.add(word); system.out.println(bag.count(word)); // prints 2 </code></pre><p>существуют похожие на map методы для перебора ключей/записей и т.д. внутренняя реализация в настоящее время использует <code>hashmap<e, atomicinteger></code>, поэтому вы не будете нести расходы на бокс.</p>

<p>вы должны знать о том, что ваша первоначальная попытка</p><pre>int count = map.containskey (word)? map.get (word): 0;</pre><p>содержит две потенциально дорогие операции на карте, а именно <code>containskey</code> и <code>get</code>. первый выполняет операцию, потенциально очень похожую на последнюю, поэтому вы выполняете ту же работу <em>дважды</em>!</p><p>если вы посмотрите на api для map, операции <code>get</code> обычно возвращают <code>null</code>, когда карта не содержит запрошенный элемент.</p><p>обратите внимание, что это сделает решение как</p><pre>map.put (ключ, map.get (ключ) + 1);</pre><p>опасно, так как это может привести к <code>nullpointerexception</code>s. сначала вы должны проверить <code>null</code>.</p><p><b>также обратите внимание</b>и это очень важно, чтобы <code>hashmap</code>s <em>может</em> содержать <code>nulls</code> по определению. так что не каждый возвращенный <code>null</code> говорит, что "такого элемента нет". в этом отношении <code>containskey</code> ведет себя <em>иначе</em> из <code>get</code>, фактически говоря вам <em>есть ли</em> есть такой элемент. обратитесь к api для деталей.</p><p>однако в вашем случае вы не захотите проводить различие между сохраненным <code>null</code> и "nosuchelement". если вы не хотите разрешать <code>null</code>s, вы можете предпочесть <code>hashtable</code>. использование библиотеки-оболочки, как уже предлагалось в других ответах, может быть лучшим решением для ручной обработки, в зависимости от сложности вашего приложения.</p><p>чтобы завершить ответ (и я забыл вставить его сначала, благодаря функции редактирования!), лучший способ сделать это изначально - это <code>get</code> в переменную <code>final</code>, проверить наличие <code>null</code> и <code>put</code> с <code>1</code> , переменная должна быть <code>final</code>, потому что она в любом случае неизменна. компилятору может не понадобиться эта подсказка, но она понятнее.</p><pre> final hashmap map = generaterandomhashmap (); final ключ объекта = fetchsomekey (); final целое число i = map.get (key); if (i ! = null) { map.put (i + 1); } else { // сделать что-то } </pre><p>если вы не хотите полагаться на автобокс, вы должны сказать что-то вроде <code>map.put(new integer(1 + i.getvalue()));</code>.</p>

<pre><code>map<string, integer> map = new hashmap<>(); string key = "a random key"; int count = map.getordefault(key, 0); map.put(key, count + 1); </code></pre><p>и вот как вы увеличиваете значение с помощью простого кода.</p><p>выгода:</p><ul> <li>не создавать другой класс для изменяемого int</li> <li>короткий номер</li> <li>легко понять</li> <li>нет исключения нулевого указателя</li> </ul><p>другой способ - использовать метод слияния, но это слишком много для простого увеличения значения.</p><pre><code>map.merge(key, 1, (a,b) -> a+b); </code></pre><p>предложение: большую часть времени вы должны заботиться о читабельности кода, а не о небольшом выигрыше в производительности.</p>

<p>другим способом было бы создание изменяемого целого числа:</p><pre><code>class mutableint { int value = 0; public void inc () { ++value; } public int get () { return value; } } ... map<string,mutableint> map = new hashmap<string,mutableint> (); mutableint value = map.get (key); if (value == null) { value = new mutableint (); map.put (key, value); } else { value.inc (); } </code></pre><p>конечно, это подразумевает создание дополнительного объекта, но издержки по сравнению с созданием integer (даже с integer.valueof) не должны быть такими большими.</p>

<p>вы можете использовать метод <a href="https://docs.oracle.com/javase/8/docs/api/java/util/map.html#computeifabsent-k-java.util.function.function-">computeifabsent</a> в интерфейсе <code>map</code>, предоставленном в <strong> java 8 </strong>.</p><pre><code>final map<string,atomiclong> map = new concurrenthashmap<>(); map.computeifabsent("a", k->new atomiclong(0)).incrementandget(); map.computeifabsent("b", k->new atomiclong(0)).incrementandget(); map.computeifabsent("a", k->new atomiclong(0)).incrementandget(); //[a=2, b=1] </code></pre><p>метод <code>computeifabsent</code> проверяет, связан ли указанный ключ со значением или нет? если связанного значения нет, то оно пытается вычислить свое значение, используя данную функцию отображения. в любом случае он возвращает текущее (существующее или вычисленное) значение, связанное с указанным ключом, или ноль, если вычисленное значение равно нулю.</p><p>напомним, что если у вас есть ситуация, когда несколько потоков обновляют общую сумму, вы можете посмотреть <a href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/longadder.html">longadder</a> class.under высокая конкуренция, ожидаемая пропускная способность этого класса значительно выше, чем <code>atomiclong</code>, за счет более высокого потребления пространства.</p>

<p>вращение памяти может быть проблемой здесь, так как каждый бокс целого числа, большего или равного 128, вызывает выделение объекта (см. integer.valueof (int)). хотя сборщик мусора очень эффективно работает с недолговечными объектами, производительность в некоторой степени пострадает.</p><p>если вы знаете, что количество сделанных приращений будет в значительной степени превосходить количество ключей (= слов в данном случае), рассмотрите возможность использования вместо этого держателя типа int. факс уже представил код для этого. здесь снова, с двумя изменениями (класс держателя сделан статическим и начальное значение установлено в 1):</p><pre><code>static class mutableint { int value = 1; void inc() { ++value; } int get() { return value; } } ... map<string,mutableint> map = new hashmap<string,mutableint>(); mutableint value = map.get(key); if (value == null) { value = new mutableint(); map.put(key, value); } else { value.inc(); } </code></pre><p>если вам нужна предельная производительность, ищите реализацию map, которая непосредственно ориентирована на примитивные типы значений. упомянутый джрудольф <a href="http://trove4j.sourceforge.net">gnu trove</a> .</p><p>кстати, хорошим поисковым термином для этой темы является "гистограмма".</p>

<p>вместо вызова функции haskey () быстрее вызывать map.get и проверять, является ли возвращенное значение нулевым или нет.</p><pre><code> integer count = map.get(word); if(count == null){ count = 0; } map.put(word, count + 1); </code></pre>

<p>есть несколько подходов:</p><ol> <li> <p>используйте сумку, как в наборах, содержащихся в google collections.</p> </li> <li> <p>создайте изменяемый контейнер, который вы можете использовать на карте:</p> </li> </ol><pre><code> class my{ string word; int count; } </code></pre><p>и использовать пут ("слово", новый мой ("слово")); затем вы можете проверить, существует ли он и увеличивается ли при добавлении.</p><p>старайтесь не использовать собственное решение, используя списки, потому что, если вы получите внутренний цикл поиска и сортировки, ваша производительность будет вонять. первое решение hashmap на самом деле довольно быстрое, но, скорее всего, такое решение, которое можно найти в google collections, лучше.</p><p>подсчет слов с помощью google collections, выглядит примерно так:</p><pre><code> hashmultiset s = new hashmultiset(); s.add("word"); s.add("word"); system.out.println(""+s.count("word") ); </code></pre><p>использование hashmultiset довольно удобно, потому что алгоритм сумок - это то, что вам нужно для подсчета слов.</p>

<p>коллекции google hashmultiset:<br> - довольно элегантный в использовании<br> - но потребляют процессор и память</p><p>лучше всего иметь такой метод, как: <code>entry<k,v> getorput(k);</code> (элегантный и недорогой)</p><p>такой метод будет вычислять хеш и индексировать только один раз, и тогда мы сможем сделать то, что мы хотим, с записью (заменить или обновить значение).</p><p>более элегантный:<br> - взять <code>hashset<entry></code><br> - расширьте его так, чтобы <code>get(k)</code> помещал новую запись, если это необходимо<br> - вход может быть вашим собственным объектом.<br> -> <code>(new myhashset()).get(k).increment();</code></p>

<p>вариант подхода mutableint, который может быть даже более быстрым, если его взломать, заключается в использовании одноэлементного массива int:</p><pre><code>map<string,int[]> map = new hashmap<string,int[]>(); ... int[] value = map.get(key); if (value == null) map.put(key, new int[]{1} ); else ++value[0]; </code></pre><p>было бы интересно, если бы вы могли повторно запустить тесты производительности с этим вариантом. это может быть самым быстрым.</p><hr><p>правка: вышеупомянутый шаблон работал хорошо для меня, но в конце концов я перешел на использование коллекций trove, чтобы уменьшить объем памяти на некоторых очень больших картах, которые я создавал - и в качестве бонуса это было также быстрее.</p><p>одна действительно приятная особенность заключается в том, что класс <code>tobjectinthashmap</code> имеет единственный вызов <code>adjustorputvalue</code>, который, в зависимости от того, есть ли уже значение в этом ключе, либо установит начальное значение, либо увеличит существующее значение. это идеально подходит для увеличения:</p><pre><code>tobjectinthashmap<string> map = new tobjectinthashmap<string>(); ... map.adjustorputvalue(key, 1, 1); </code></pre>

<p>я думаю, что ваше решение будет стандартным, но, как вы сами отметили, это, вероятно, не самый быстрый способ.</p><p>вы можете посмотреть на <a href="http://trove4j.sourceforge.net">gnu trove</a> . это библиотека, которая содержит все виды быстрых примитивных коллекций. ваш пример будет использовать <a href="http://trove4j.sourceforge.net/javadocs/gnu/trove/map/hash/tobjectinthashmap.html">tobjectinthashmap</a> , который имеет метод adjustorputvalue, который делает именно то, что вы хотите.</p>

<p>вы уверены, что это узкое место? вы провели анализ производительности?</p><p>попробуйте использовать профилировщик netbeans (он бесплатный и встроен в nb 6.1) для просмотра горячих точек.</p><p>наконец, обновление jvm (скажем, с 1.5-> 1.6) часто является дешевым средством повышения производительности. даже обновление номера сборки может обеспечить хорошее повышение производительности. если вы работаете в windows, и это приложение серверного класса, используйте -server в командной строке, чтобы использовать jvm server hotspot. на машинах linux и solaris это определяется автоматически.</p>

<p>все очень просто, просто используйте встроенную функцию в <code>map.java</code> как</p><pre class="lang-java prettyprint-override"><code>map.put(key, map.getordefault(key, 0) + 1); </code></pre>

<p>"поставить" нужно "получить" (чтобы избежать дублирования ключа).<br> так что прямо делай "пут",<br> и, если было предыдущее значение, сделайте дополнение:</p><pre><code>map map = new hashmap (); mutableint newvalue = new mutableint (1); // default = inc mutableint oldvalue = map.put (key, newvalue); if (oldvalue != null) { newvalue.add(oldvalue); // old + inc } </code></pre><p>если count начинается с 0, то добавьте 1: (или любые другие значения ...)</p><pre><code>map map = new hashmap (); mutableint newvalue = new mutableint (0); // default mutableint oldvalue = map.put (key, newvalue); if (oldvalue != null) { newvalue.setvalue(oldvalue + 1); // old + inc } </code></pre><p><em>примечание:</em> этот код не является потокобезопасным. используйте его, чтобы построить, а затем использовать карту, а не обновлять ее одновременно.</p><p><em>оптимизация:</em> в цикле сохраните старое значение, чтобы оно стало новым значением следующего цикла.</p><pre><code>map map = new hashmap (); final int defaut = 0; final int inc = 1; mutableint oldvalue = new mutableint (default); while(true) { mutableint newvalue = oldvalue; oldvalue = map.put (key, newvalue); // insert or... if (oldvalue != null) { newvalue.setvalue(oldvalue + inc); // ...update oldvalue.setvalue(default); // reuse } else oldvalue = new mutableint (default); // renew } } </code></pre>

<p>если вы используете <a href="http://www.eclipse.org/collections/">eclipse collections</a> , вы можете использовать <code>hashbag</code>. это будет наиболее эффективный подход с точки зрения использования памяти, а также он будет хорошо работать с точки зрения скорости выполнения.</p><p><code>hashbag</code> поддерживается <code>mutableobjectintmap</code>, в котором хранятся примитивные целые числа вместо объектов <code>counter</code>. это уменьшает накладные расходы памяти и повышает скорость выполнения.</p><p><code>hashbag</code> предоставляет api, который вам нужен, так как это <code>collection</code>, который также позволяет вам запрашивать количество вхождений элемента.</p><p>вот пример из <a href="https://github.com/eclipse/eclipse-collections-kata">eclipse collections kata</a> .</p><pre><code>mutablebag<string> bag = hashbag.newbagwith("one", "two", "two", "three", "three", "three"); assert.assertequals(3, bag.occurrencesof("three")); bag.add("one"); assert.assertequals(2, bag.occurrencesof("one")); bag.addoccurrences("one", 4); assert.assertequals(6, bag.occurrencesof("one")); </code></pre><p><strong> примечание: </strong> я являюсь коммиттером для коллекций eclipse.</p>

<p>я бы использовал ленивую карту коллекций apache (чтобы инициализировать значения 0) и использовал mutableintegers из apache lang в качестве значений на этой карте.</p><p>самой большой ценой является то, что вам придется дважды искать карту в вашем методе. по моему вы должны сделать это только один раз. просто получите значение (оно будет инициализировано, если оно отсутствует) и увеличьте его.</p>

<p>в структуре <a href="http://code.google.com/p/functionaljava">функциональная java</a> <code>treemap</code> библиотеки есть метод <code>update</code> в последней заголовке соединительной линии:</p><pre><code>public treemap<k, v> update(final k k, final f<v, v> f) </code></pre><p>пример использования:</p><pre><code>import static fj.data.treemap.empty; import static fj.function.integers.add; import static fj.pre.ord.stringord; import fj.data.treemap; public class treemap\_update {public static void main(string[] a) {treemap<string, integer> map = empty(stringord); map = map.set("foo", 1); map = map.update("foo", add.f(1)); system.out.println(map.get("foo").some());}} </code></pre><p>эта программа печатает "2".</p>

<p>я не знаю, насколько это эффективно, но приведенный ниже код также работает. вам нужно определить <code>bifunction</code> в начале. кроме того, вы можете сделать больше, чем просто увеличить с помощью этого метода.</p><pre><code>public static map<string, integer> strint = new hashmap<string, integer>(); public static void main(string[] args) { bifunction<integer, integer, integer> bi = (x,y) -> { if(x == null) return y; return x+y; }; strint.put("abc", 0); strint.merge("abc", 1, bi); strint.merge("abc", 1, bi); strint.merge("abc", 1, bi); strint.merge("abcd", 1, bi); system.out.println(strint.get("abc")); system.out.println(strint.get("abcd")); } </code></pre><p>результат</p><pre><code>3 1 </code></pre>

<p>различные примитивные оболочки, например, <code>integer</code>, являются неизменяемыми, поэтому на самом деле нет более краткого способа сделать то, что вы просите <em>, если </em> вы не можете сделать это с чем-то как <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/atomic/atomiclong.html">atomiclong</a> . я могу дать это за минуту и обновить. кстати, <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/hashtable.html">hashtable</a> <em> является </em> частью <a href="http://java.sun.com/j2se/1.5.0/docs/guide/collections/index.html">collections framework</a> .</p>

<p>@vilmantas baranauskas: что касается этого ответа, я бы прокомментировал, если бы у меня были точки повторения, но у меня его нет. я хотел бы отметить, что определенный здесь класс counter не является потокобезопасным, так как недостаточно просто синхронизировать inc () без синхронизации value (). другие потоки, вызывающие value (), не гарантированно увидят значение, если с обновлением не было установлено отношение "происходит до".</p>