Кратко
Когда дело доходит до разницы между HashMap и Hashtable, вы можете подумать, что первый не является потокобезопасным, а второй — потокобезопасным. Но когда нам нужна безопасность потоков, Hashtable — не лучший выбор, а concurrentHashMap.
Зачем использовать concurrentHashMap
Получить потокобезопасный HashMap можно тремя способами:
① Используйте хэш-таблицу
② Используйте метод Collections.synchronizedMap()
③ Используйте concurrentHashMap
Почему с этими тремя методами рекомендуется третий метод вместо двух других? Ответ: Эффективность, давайте посмотрим на исходный код этих двух методов.
Мы видим, что ключевое слово synchronized также используется для переноса HashMap в потокобезопасную карту, аналогичную Hashtable, только один поток может получить доступ к этому объекту за раз.public static Map synchronizedMap(Map m) { return new SynchronizedMap<>(m); }
скопировать кодprivate static class SynchronizedMap<K,V> implements Map<K,V>, Serializable { private final Map<K,V> m; // Backing Map final Object mutex; // Object on which to synchronize SynchronizedMap(Map<K,V> m) { this.m = Objects.requireNonNull(m); mutex = this; } public int size() { synchronized (mutex) {return m.size();} } public boolean isEmpty() { synchronized (mutex) {return m.isEmpty();} } public boolean containsKey(Object key) { synchronized (mutex) {return m.containsKey(key);} }
И ConcurrentHashMap jdk1.7 принимаетблокировка сегментатехнология, где Segment наследуется от ReentrantLock. Синхронизация не требуется как для операций ввода, так и для операций получения, таких как HashTable. Блокировка сегмента сегмента была отменена в jdk1.8 и принятаCAS + synchronizedдля обеспечения безопасности резьбы.
Разбор ConcurrentHashMap (JDK 1.8)
Атрибуты
/**
* 存放node的数组,大小是2的幂次方
*/
transient volatile Node[] table;
/**
* 扩容时用于存放数据的变量,平时为null
*/
private transient volatile Node[] nextTable;
/**
* 通过CAS更新,记录容器的容量大小
*/
private transient volatile long baseCount;
/**
* 控制标志符
* 负数: 代表正在进行初始化或扩容操作,其中-1表示正在初始化,-N 表示有N-1个线程正在进行扩容操作
* 正数或0: 代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,类似于扩容阈值
* 它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。
* 实际容量 >= sizeCtl,则扩容
*/
private transient volatile int sizeCtl;
/**
* 下次transfer方法的起始下标index加上1之后的值
*/
private transient volatile int transferIndex;
/**
* CAS自旋锁标志位
*/
private transient volatile int cellsBusy;
/**
* counter cell表,长度总为2的幂次
*/
private transient volatile CounterCell[] counterCells;
важный внутренний класс
static class Node implements Map.Entry {
final int hash;
final K key;
volatile V val;
volatile Node next;
...
}
static final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; // red-black tree links
TreeNode left;
TreeNode right;
TreeNode prev; // needed to unlink next upon deletion
boolean red;
...
}
static final class TreeBin extends Node {
TreeNode root;
volatile TreeNode first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
...
}
static final class ForwardingNode extends Node {
final Node[] nextTable;
//ForwardingNode节点hash为-1,若操作中遇到此类型节点,表明有线程正在扩容
ForwardingNode(Node[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
...
}
Метод строительства
/**
* 默认构造方法
*/
public ConcurrentHashMap() {}
/**
* 指定容量
*/
public ConcurrentHashMap(int initialCapacity) {
//异常情况
if (initialCapacity < 0)
throw new IllegalArgumentException();
//计算容量
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
/**
* 指定map
*/
public ConcurrentHashMap(Map m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
/**
* 指定容量、负载因子
*/
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
/**
* 指定容量、负载因子、并发级别
*/
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
CAS-операция
concurrentHashMap вызывает метод класса Unsafe для реализации CAS (Основная идея этого алгоритма заключается в том, чтобы постоянно сравнивать, равно ли значение переменной в текущей памяти значению указанной вами переменной.Если они равны, принять указанное вами измененное значение, в противном случае отклонить вашу операцию.), большинство его внутренних методов являются собственными методами, которые напрямую вызывают базовые ресурсы операционной системы для выполнения соответствующих задач, предоставляя некоторые базовые операции, которые могут напрямую манипулировать памятью и потоками.
метод initTable
Метод initTable оценивает значение sizeCtl, если значение sizeCtl равно -1,Другие потоки инициализируются, вызовите Thread.yield(), чтобы отказаться от кванта времени ЦП, а инициализируемый поток изменяет sizeCtl на -1 с помощью метода Unsafe.compareAndSwapInt, инициализирует массив и порог расширения.
private final Node[] initTable() {
Node[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//若sizeCtl<0,即存在其他线程正在初始化操作,确保只有一个线程进行初始化
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//利用CAS方法把sizectl的值置为-1,表明已有线程进行初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
//获得桶容量
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//初始化node数组
Node[] nt = (Node[])new Node[n];
table = tab = nt;
//计算扩容阈值0.75n
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
Из исходного кода мы можем видеть, что самый внешний элемент управления процессом использует цикл while вместо условной ветви if, потому что Thread.yield() откажется от процессорного времени, чтобы убедиться, что массив успешно инициализирован.
положить метод
поставить операцию ConcurrentHashMapочень похоже на HashMap, но ConcurrentHashMapНе разрешать null в качестве ключа и значения, и из-за необходимости обеспечения безопасности потоков существуют следующие две многопоточные ситуации:
① Если один или несколько потоков расширяют ConcurrentHashMap, текущий поток также должен начать операцию расширения. Причина, по которой эта операция расширения может быть обнаружена, заключается в том, что метод передачи установит головной узел сегмента расширения, который использовался как узел ForwardingNode.Если он обнаружит, что вставляемая позиция занята узлом, это поможет для расширения емкости.
② Если обнаруживается, что вставляемый узел не является пустым и не является узлом ForwardingNode, узел блокируется, что обеспечивает безопасность потоков.
final V putVal(K key, V value, boolean onlyIfAbsent) {
//与HashMap不同,ConcurrentHashMap不允许null作为key或value
if (key == null || value == null) throw new NullPointerException();
//计算hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node[] tab = table;;) {
Node f; int n, i, fh;
//若table为空的话,初始化table
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//若当前数组i位置上的节点为null
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//CAS插入节点(V:当前数组i位置上的节点; O:null; N:新Node对象)
if (casTabAt(tab, i, null,
new Node(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//当前正在扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//锁住当前数组i位置上的节点
synchronized (f) {
//判断是否节点f是否为当前数组i位置上的节点,防止被其它线程修改
if (tabAt(tab, i) == f) {
//当前位置桶的结构为链表
if (fh >= 0) {
binCount = 1;
//遍历链表节点
for (Node e = f;; ++binCount) {
K ek;
//若hash值与key值相同,进行替换
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node pred = e;
//若链表中找不到,尾插节点
if ((e = e.next) == null) {
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
//当前位置桶结构为红黑树,TreeBin哈希值固定为-2
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
//遍历红黑树上节点,更新或增加节点
if ((p = ((TreeBin)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//若链表长度超过8,将链表转为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//节点数+1,若超过阈值则扩容
addCount(1L, binCount);
return null;
}
/**
* hash算法
*/
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
Грубый процесс:
① Сначала оцените, являются ли ключ и значение нулевыми, и создайте исключение для нулевого значения.
В отличие от HashMap, ConcurrentHashMap не допускает null в качестве ключа или значения, почему он разработан таким образом?
Поскольку ConcurrentHashmap поддерживает параллелизм, возникнет проблема. Когда вы получаете соответствующее значение через get(k), если вы получаете null, вы не можете судить. Когда он помещен (k, v), значение равно null или этот ключ никогда не отображался. HashMap не является параллельным и может оцениваться по содержанию (ключу). Хотя карта, поддерживающая параллелизм, вызывает m.contains(key) и m.get(key), m может отличаться.Ссылаться на
②. Пересчитайте хеш-значение, потому что 1.8concurrentHashMap вводит красно-черное дерево для обработки большего количества коллизий хэшей, что упрощает алгоритм хеширования, но по сравнению с хэш-алгоритмом 1.8HashMap, за исключением того, что concurrentHashMap не допускает null в качестве ключа, null не используется В дополнение к хеш-значению 0, он имеет еще одну битовую операцию. Причина этого заключается в том, чтобы гарантировать, что вычисление повторного хеширования не должно быть отрицательным числом. Я думаю, это потому, что если отрицательное значение рассчитывается, это может повлиять на последующую оценку типа узла.
③. Определите, пуста ли текущая таблица. Если она пуста, инициализируйте таблицу. Метод инициализации был объяснен и больше не упоминается.
④ В соответствии со значением, рассчитанным с помощью повторного хеширования, индекс корзины получается с помощью операции И, а класс Unsafe используется для прямого получения узла в соответствующей позиции в памяти.Если нет коллизии, есть нет узла в ведре. CAS добавляется напрямую
⑤ Если есть коллизия, определите тип первого узла в ведре. Если это узел ForwardingNode, это указывает, что другие потоки расширяются, и операция расширения выполняется вместе.
⑥. Оставшаяся структура узла узла списка сцены, соответствующая или красно-черное дерево вставить или обновить новый узел
⑦ Если длина связанного списка после добавления узла больше 8, преобразовать связанный список в красно-черное дерево.
⑧ Последнее количество узлов равно +1, проверьте, превышает ли оно пороговое значение, и увеличьте емкость, если оно превышает
метод addCount
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//利用CAS方法更新baseCount的值
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//多线程CAS发生失败的时候执行
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
///如果check值大于等于0 则需要检验是否需要进行扩容操作
if (check >= 0) {
Node[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
Мы видим, что две операции CAS используются для обновления baseCount, одна напрямую обновляет basecount, и если обновление не удается, обновляется CELLVALUE, и если обновление завершается неудачей, вызывается метод fullAddCount(). сбой сохраняется в массиве CounterCell для подготовки к методу size().
метод размера
Для ConcurrentHashMap количество вещей в этой таблице на самом деле является неопределенной суммой, потому чтоНевозможно остановить другие потоки, чтобы вы считали, как GC «остановить мир» при вызове метода size(), поэтому можно только сказать, что это число является оценочным.
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
Чтобы подсчитать количество элементов, ConcurrentHashMap получает общее количество элементов, накапливая числа в массивах baseCount и CounterCell, сохраняя количество элементов в baseCount и сохраняя количество изменений некоторых элементов в массиве CounterCell.
способ передачи
Он поддерживает многопоточное расширение без блокировки. Цель состоит не только в том, чтобы удовлетворить требования параллельной обработки, но и в том, чтобы использовать параллельную обработку для сокращения времени, затрачиваемого на расширение.
Следующие два сценария могут активировать механизм расширения (такой же, как 1.8HashMap):
① Когда длина связанного списка в корзине достигает порога 8, но количество узлов во всем ConcurrentHashMap меньше 64
② После добавления узлов количество узлов во всей ConcurrentHashMap превышает пороговое значение.
private final void transfer(Node[] tab, Node[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
// 创建node数组,容量为当前的两倍
Node[] nt = (Node[])new Node[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
// 若扩容时出现OOM异常,则将阈值设为最大,表明不支持扩容
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
// 创建ForwardingNode节点,作为标记位,表明当前位置桶已做过处理
ForwardingNode fwd = new ForwardingNode(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//通过CAS设置transferIndex属性值,并初始化i和bound值
//i指当前处理的槽位序号,bound指需要处理的槽位边界
//先处理最后一个桶的节点;
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 将原数组中节点复制到新数组中去
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//如果所有的节点都已经完成复制工作 就把nextTable赋值给table 清空临时对象nextTable
if (finishing) {
nextTable = null;
table = nextTab;
//设置新扩容阈值
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//利用CAS方法更新扩容阈值,在这里面sizectl值减一,说明新加入一个线程参与到扩容操作
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
//锁住i位置上桶的节点
synchronized (f) {
//确保f是i位置上桶的节点
if (tabAt(tab, i) == f) {
Node ln, hn;
//当前桶是链式结构
if (fh >= 0) {
//构造两个链表
int runBit = fh & n;
Node lastRun = f;
//类似于1.8HashMap,只需要看新增的1bit是0还是1进行分类
for (Node p = f.next; p != null; p = p.next) {
//n是就数组长度,不是长度-1
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node(ph, pk, pv, ln);
else
hn = new Node(ph, pk, pv, hn);
}
//在nextTable的i位置上插入一个链表
setTabAt(nextTab, i, ln);
//在nextTable的i+n的位置上插入另一个链表
setTabAt(nextTab, i + n, hn);
//在table的i位置上插入forwardNode节点 表示已经处理过该节点
setTabAt(tab, i, fwd);
//设置advance为true 返回到上面的while循环中 就可以执行i--操作
advance = true;
}
//当前桶是红黑树结构,操作和上面的类似
else if (f instanceof TreeBin) {
TreeBin t = (TreeBin)f;
TreeNode lo = null, loTail = null;
TreeNode hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode p = new TreeNode
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
//如果扩容后已经不再需要tree的结构 反向转换为链表结构
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
Как показано (Blue: Hash Value X) 0, серый: хеш-значение x] 1)
Первый обход классификации связанного списка Используйте fn&n, чтобы быстро разделить элементы в связанном списке на две категории, просто посмотрите, равен ли новый 1bit 0 или 1, runBit равен 0, lastRun — это узел F, ln — это узел F и hn равно нулюПосле повторного обхода связанного списка первая цепочка: B→A→F, hn цепочка: E→D→C (Один прямой связанный список, один обратный связанный список)
Непосредственно управляйте памятью через класс Unsafe, установите связанный список ln в позицию i нового массива и установите связанный список hn в позицию i+n.
Измените узел позиции исходного массива i на узел ForwardingNode, указывающий, что ведро обработано.
(1) Если количество элементов в связанном списке lo меньше или равно UNTREEIFY_THRESHOLD, значение по умолчанию равно 6, то связанный список узлов дерева преобразуется в обычный связанный список узлов методом untreeify;
(2) В противном случае оцените, равно ли число элементов в высокосвязном списке 0: если оно равно 0,Указывает, что все узлы в исходном сегменте все еще находятся в этом сегменте после повторного хэширования, то есть все исходные узлы включены в связанный список lo., затем установите для исходного красно-черного дерева значение ln, в противном случае восстановите красно-черное дерево в соответствии со связным списком lo.
После прочтения метода расширения JDK8 HashMap он должен показаться немного похожим ConcurrentHashMap обрабатывает структуру связанного списка, аналогичную последней части метода resize() HashMap, и обрабатывает красно-черную древовидную структуру, аналогичную методу split(). внутреннего класса TreeNode в HashMap и метод построения TreeBin.Похож на метод treeify() внутреннего класса TreeNode, но его работа намного сложнее, поскольку ConcurrentHashMap должен обеспечивать безопасность потоков
Вопрос: Зачем нужно строить список?
Ссылаясь на идею 1,7 узла вставки головки HASHMAP, поскольку более поздние вставленные данные используются чаще, его нельзя доступны случайным образом и могут проходить только через запрос с самого начала, в то время как CONCURRENTHASHMAP использует встроенный блокировку, синхронизированный для блокировки встроенного блокировки Узел для головы ведра, потому что более поздние вставленные данные более часто используются,Обратный порядок сокращает время удержания блокировки и время ожидания.(Только мое мнение)
получить метод
Когда ключ дается для определения значения, должны быть выполнены два условия. Ключ тот же и хеш-значение такое же. В случае, если узел может быть в связанном списке или дереве, его необходимо найти. по отдельности.
public V get(Object key) {
Node[] tab; Node e, p; int n, eh; K ek;
//计算hash值
int h = spread(key.hashCode());
////根据hash值确定节点位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//桶首节点的key与查找的key相同,则直接返回
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//树节点场景
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//链表场景
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
Разница между ConcurrentHashMap 1.7 и 1.8
Независимо от деталей, concurrenthashmap в JDK 7 в основном использует сегменты. Мульти-резьбовое соревнование будет сначала заблокировать сегмент. В своей операции положения сегмент будет расположен первым, а затем определенное положение ведра в сегменте будет расположена, пока JDK 8 напрямую манипулирует массив узла. Ведро заблокировано, а узел головки ведра заблокирован, а гранулярность блокировки становится меньше. Кроме того, сегмент наследует ReentrantLock, и ReentrantLock имеет более ключевые слова, чем синхронизированныеОжидание с прерыванием, справедливое, связывание нескольких условий, а по производительности, т.к синхронизированный встроенный замок постоянно оптимизируется, производительность у него будет неплохая.
благодарный
woo woo Краткое описание.com/fear/oh 694 отправить 1 oh 86…
blog.CSDN.net/itachi85/AR…