java多线程之并发容器

ConcurrentHashMap

java7版本

ConcurrentHashMap在java7实现的原理是基于segment+拉链法+ReentrantLock:

整体put流程:
1 计算哈希值

当调用 put 方法时,首先会计算键的哈希值,以确定它属于哪个段。哈希值被用来确定键值对应该放置在哪个段中。

int hash = key.hashCode();
int segmentIndex = (hash >>> segmentShift) & segmentMask;

这里 segmentShiftsegmentMask 是根据段的数量预先计算出来的值。

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();
  • 使用场景

    • 适用于定时任务或事件的调度。
    • 适合用于实现定时任务队列,其中任务在到达指定时间后才会被处理。
  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值