ConcurrentHashMap
java7版本
ConcurrentHashMap在java7实现的原理是基于segment+拉链法+ReentrantLock:
整体put流程:
1 计算哈希值
当调用 put
方法时,首先会计算键的哈希值,以确定它属于哪个段。哈希值被用来确定键值对应该放置在哪个段中。
int hash = key.hashCode();
int segmentIndex = (hash >>> segmentShift) & segmentMask;
这里 segmentShift
和 segmentMask
是根据段的数量预先计算出来的值。
2. 获取锁
一旦确定了键应该放入哪个段,就会获取该段的锁。
Segment<K,V> s;
if ((s = segments[segmentIndex]) == null)
s = ensureSegment(segmentIndex);
s.lock(); // 获取段锁
try {
// 接下来的操作在锁定范围内
} finally {
s.unlock(); // 释放段锁
}
3. 插入元素
在锁定范围内,put
操作会检查是否有现有元素与要插入的键相等。如果没有,则将新元素插入到适当的位置。如果有,则更新现有的值。
4.hash冲突
当在某个segment的某个位置发生了hash冲突,会将元素插入到该位置节点的下一个节点形成一个链表。
ps:Segment的数量不能超过16,产生hash冲突时使用的是拉链法
java8版本
java8的ConcurrentHashMap是基于红黑树+链表+CAS锁+Synchronized实现的:
整体put流程:
1. 初始化容量
在添加元素之前,如果数组尚未初始化,ConcurrentHashMap
会尝试初始化一个初始容量的数组。
if (tab == null || (n = tab.length) == 0)
n = (tab = resize()).length;
resize()
方法用于初始化数组或重新散列。
2. 计算哈希值
计算键的哈希值,并确定元素应该插入到数组中的哪个槽(bucket)。
int h = hash(key);
int i = (n - 1) & h;
这里的 hash()
方法会对键的原始哈希值进行扰动,以减少哈希冲突。
3. 插入元素
接下来尝试插入元素。这里使用了 CAS 操作来尝试设置数组中的元素,如果 CAS 成功,则插入完成。否则,会进入内部循环处理冲突。
1for (Node<K,V> e = tabAt(tab, i); e != null; e = next(e)) {
2 int eh; K ek;
3 // 检查键是否相等
4 if (e.hash == h && ((ek = key(e)) == key || (key != null && key.equals(ek)))) {
5 V ev = e.val;
6 if (!onlyIfAbsent || ev == null)
7 casVal(e, ev, remappingFunction.apply(ev));
8 afterNodeAccess(e);
9 return remappingFunction.apply(ev);
10 }
11}
如果循环中没有找到相等的键,则尝试插入新节点。
Node<K,V> newNode = new Node<K,V>(h, key, value, null);
casTabAt(tab, i, null, newNode);
if (casTabAt(tab, i, null, newNode)) {
afterNodeInsertion(true);
return value;
}
4. 处理冲突
如果 CAS 设置失败(因为另一个线程已经设置了该位置),则需要处理冲突。这可能涉及以下步骤:
- 如果节点已经是树节点,则尝试在树中插入。
- 如果节点是链表中的最后一个节点,则尝试插入新节点。
- 如果链表长度超过阈值(默认为 8),则将链表转换为树。
if (e == null) { // 如果当前槽为空
if (casTabAt(tab, i, null, newNode)) {
afterNodeInsertion(false);
return null;
}
} else { // 如果当前槽已经有元素
if (e instanceof TreeBin) {
TabTreeNode<K,V> t = ((TreeBin<K,V>)e).root;
if (t == null) {
TabTreeNode<K,V> newTreeNode = new TabTreeNode<>(h, key, value, null);
casTabAt(tab, i, e, new TreeBin<>(newTreeNode));
afterNodeInsertion(false);
return null;
}
TabTreeNode<K,V> p = t.insert(h, key, value, tab, n);
if (p.prev == null)
casTabAt(tab, i, e, new TreeBin<>(p));
return p.val;
} else if (e.hash == h && ((ek = key(e)) == key || (key != null && key.equals(ek)))) {
V ev = e.val;
if (!onlyIfAbsent || ev == null)
casVal(e, ev, remappingFunction.apply(ev));
afterNodeAccess(e);
return remappingFunction.apply(ev);
} else {
TabListNode<K,V> lastRun = e;
TabListNode<K,V> lastRunOfLength = lastRun;
int lastLen = 1;
for (TabListNode<K,V> p = e.next; p != null; p = p.next) {
int b = (p.hash & n) == 0 ? 0 : 1;
if (b == ((lastRun.hash & n) == 0 ? 0 : 1)) {
lastLen++;
} else {
if (lastLen >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, n, e);
return null;
}
lastRunOfLength = lastRun;
lastLen = 1;
}
lastRun = p;
}
if (lastLen >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, n, e);
return null;
}
if (lastRun == e) {
TabListNode<K,V> p = new TabListNode<>(h, key, value, null);
casNext(e, e.next, p);
afterNodeInsertion(false);
return null;
} else if (lastRunOfLength == lastRun) {
TabListNode<K,V> p = new TabListNode<>(h, key, value, null);
casNext(lastRun, lastRun.next, p);
afterNodeInsertion(false);
return null;
} else {
TabListNode<K,V> p = new TabListNode<>(h, key, value, lastRun.next);
casNext(lastRunOfLength, lastRunOfLength.next, p);
casNext(lastRun, p, null);
afterNodeInsertion(false);
return null;
}
}
}
ps:在链表长度超过了8的时候会自动转为红黑树,在链表长度小于8的时候又会转为链表,这是因为链表的查询速度弱于红黑树,但红黑树占用内存弱于链表,这样做可以实现时空均衡
HashTable为什么会被弃用:Hashtable的每一个方法都加了Synchronized,同时也不允许在迭代期间修改值,一改就抛出异常,这些特性在高并发的时候会大大减低系统性能。
CopyOnWriteArrayList
CopyOnWriteArrayList容器在读多写少的场景,可谓是性能极高,它的读取没有锁,可以同时读写,在需要写入元素时,CopyOnWriteArrayList会复制出一份新的表并获取ReentratLock(可重入锁),写入的数据是写入到新复制的表中,然后再将引用指向新的表:
public final void add(E e) {
final ReentrantLock lock = this.lock;//获取锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//复制
newElements[len] = e;//写入
setArray(newElements);//更改引用
} finally {
lock.unlock();
}
}
缺点:
1.数据不一致: 在写入的时候,读的线程读的是旧数据,不能及时感知数据发生了变化。
2.复制占用空间和时间:当表的元素变得很多的时候,复制会带来额外的开销。
阻塞队列:
1. ArrayBlockingQueue
-
特点:基于数组结构的有界阻塞队列。
-
容量限制:队列的最大容量是固定的,可以通过构造函数指定。
-
公平性:可以指定是否支持公平的 FIFO 顺序。
-
用法:
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10); queue.put("item"); // 添加元素,如果队列已满,则阻塞 String item = queue.take(); // 移除并返回队列头部的元素,如果队列为空,则阻塞
-
使用场景:
- 适用于有固定大小的缓存场景。
- 适合用于资源有限的系统中,防止资源耗尽。
2. LinkedBlockingQueue
-
特点:基于链表结构的阻塞队列。
-
容量限制:可以选择固定容量或无限容量。
-
公平性:总是使用非公平的 FIFO 顺序。
-
用法:
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(10); queue.put("item"); String item = queue.take();
-
使用场景:
- 适用于不确定队列大小的场景。
- 适合用于消息队列或任务队列,特别是当队列大小难以预估时。
3. PriorityBlockingQueue
-
特点:基于优先级堆(通常是小顶堆)的无界阻塞队列。
-
容量限制:默认是无限容量。
-
公平性:元素按照自然排序或提供的比较器排序。
-
用法:
PriorityBlockingQueue<String> queue = new PriorityBlockingQueue<>(); queue.put("item1"); queue.put("item2"); String item = queue.take();
-
使用场景:
- 适用于需要按优先级顺序处理元素的场景。
- 适合用于任务调度或事件处理,其中某些任务或事件具有更高的优先级。
4. SynchronousQueue
-
特点:不存储元素的阻塞队列,每个插入操作必须等待另一个线程调用移除操作,反之亦然。
-
容量限制:没有存储空间。
-
公平性:可以选择公平或非公平的模式。
-
用法:
SynchronousQueue<String> queue = new SynchronousQueue<>(); queue.put("item"); // 直接传递给等待接收的线程 String item = queue.take(); // 等待下一个插入操作
-
使用场景:
- 适用于传递对象的场景,特别是当需要直接从生产者传递给消费者时。
- 适合用于构建管道模式或实现线程间的数据传递。
5. DelayQueue
-
特点:保存了延迟到期的对象的无界阻塞队列。
-
容量限制:默认是无限容量。
-
公平性:元素按照到期时间排序。
-
用法:
DelayQueue<DelayedElement> queue = new DelayQueue<>(); DelayedElement element = new DelayedElement(1000L); // 1秒后到期 queue.put(element); DelayedElement item = queue.take();
-
使用场景:
- 适用于定时任务或事件的调度。
- 适合用于实现定时任务队列,其中任务在到达指定时间后才会被处理。