Java并发笔记 (13)------ java 高并发容器

Java并发容器

吐槽一下:面试前真要准备好源代码分析,看源码真的是一种享受!

一、ConcurrentHashMap

为什么要用ConcurrentHashMap?

1. HashMap的线程不安全

ConcurrentHashMap 是线程安全且高效的 HashMap

我们看一下下面的代码

@Test
public void hashThreadunsafe() throws InterruptedException {
    final HashMap<String, String> map = new HashMap<String, String>(2);
    for (int i = 0; i < 100; i++) {

    }
    Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 1000000; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        map.put(UUID.randomUUID().toString(), "");
                    }
                }, "ftf" + i).start();
            }
        }
    }, "ftf");
    t.start();
    t.join();
}

重点!!!!!!

重点!!!!!!

重点!!!!!!

jdk 1.7中, 多线程不断插入数据会导致HashMap的transfer()过程, 我们看下面源代码可以看到,使用的是头插法,我们注意一下的代码处,如果rehash过程中挂起,那么其他线程在进行rehash过程时会形成一个循环链表,导致当前线程执行时,进入死循环!

transfer()源码如下:

void transfer(Entry[] newTable, boolean rehash) {  
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) { //遍历所有桶
        while(null != e) {  
            Entry<K,V> next = e.next;  
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);  
            }  
            int i = indexFor(e.hash, newCapacity);//定位Hash桶  
            e.next = newTable[i];
            newTable[i] = e;//线程在此处挂起!!!!!
            e = next;
        }  
    }  
}  

下面我们看一下 jdk1.8的情况(此处作者查看了一下jdk10的源代码,跟jdk1.8一致),通过尾插法,很好的避免了死锁的情况,但是仍然会出现线程不安全的情况!!同样我们看一下的位置。

如果线程A,B都在进行put操作,并且hash值相同的。此时线程A执行到处的代码后由于执行代码太累了想去吃瓜被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A上工,继续执行代码,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全

下面为HashMap的 putVal() 使用尾插法源码

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素!!!!!!
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 判断 entry头的类型,如果为TreeNode则进行红黑树插入
        // TreeNode extends HashMap.Node
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 此处采用链表尾插法!
            // 这里会出现循环的问题
            for (int binCount = 0; ; ++binCount) {
                // 1.
                if ((e = p.next) == null) {
                    // 2.
                    p.next = newNode(hash, key, value, null);
                    // 如果entry[hash]链表长度大于8,则转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //...
    }
    // ...
    return null;
}

resize() 源代码

final Node<K,V>[] resize() {
   //....
    do {
        next = e.next;
        if ((e.hash & oldCap) == 0) { // 这里采用尾插法
            if (loTail == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
        }
        else {
            if (hiTail == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
        }
    } while ((e = next) != null);
    if (loTail != null) {
        loTail.next = null;
        newTab[j] = loHead;
    }
    if (hiTail != null) {
        hiTail.next = null;
        newTab[j + oldCap] = hiHead;
    }
    //...
}
2. HashTable 效率低下

HashTable容器使用 synchronized 来保证线程安全。但是效率低下!为什么呢?看一下源代码

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}

可以看到此处用synchronized来对访问的线程加锁。

想到一个问题:那多线程怎么保证可以访问到数据呢?

private static class Entry<K,V> implements Map.Entry<K,V>

这样定义,使得数据被加载到方法区中,方法区线程共享。这样就可以访问到数据了!

源码分析

这里又是一个坑!常被问jdk1.6 、1.8的区别?(这里主要分析jdk1.8)

首先jdk1.6使用分段锁,将数据分成16段,这样使得对比HashTable并发量提高了16倍!!!

1.我们先看一下jdk1.6结构图及分析:

在这里插入图片描述

在这里插入图片描述

下面通过源码来看一下如何定位segment?

private static int hash(int h) {
    h += (h << 15) ^ 0xffffcd7d;
    h ^= (h >>> 10);
    h += (h << 3);
    h ^= (h >>> 6);
    h += (h << 2) + (h << 14);
    return h ^ (h >>> 16);
}
final Segment<K,V> segmentFor(int hash) {
    // segmentShift为28 segmentMask为15
    // 由于segment为16个 所以需要保留四位!
	return segments[(hash >>> segmentShift) & segmentMask];
}

使用再散列,减少散列冲突

下面大致说一下确定到segment后,使用ReentrantLockSegment加锁,保证其他进程访问时被Block住,保证Segment之间的同步。

如何统计segment中的大小呢?!

在put、remove和clean方法里操作元素前都会将变量modCount进行加1,那么在计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

2. jdk 1.8

jdk 1.8 为每个Entry使用sychronized 锁,大大提升了并发效率。

如果有n个Entry的Map,则并发量为n

放上结构图:

在这里插入图片描述

源代码

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 使用CAS保证 节点为空时的原子操作
            // 使用了Unsafe类的本地方法
            if (casTabAt(tab, i, null,
                         new Node<K,V>(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;
            // 为每个桶加锁 保证同步
            synchronized (f) {
                //......
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

那怎么确保rehash过程中的线程安全呢?

private final void transfer(Node<K,V>[] tab, Node<K,V>[] 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<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            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;
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            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
            }
        }
        // 如果rehash为空,则使用CAS原子操作来添加节点
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            synchronized (f) {
                // ...
                // 跟HashMap过程类似,通过同步锁来确保不会有数据丢失!
            }
        }
    }
}

分析

主要看标记的位置,ConcurrentHashMapHashMap的基础上 保证了transfer()过程的线程安全!

二、 ConcurrentLinkedQueue

结构分析

通过ConcurrentLinkedQueue的类图来分析一下它的结构

在这里插入图片描述
ConcurrentLinkedQueuehead节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点(next)的引用组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。

private static class Node<E> {
    volatile E item;
    volatile Node<E> next;

   
    Node(E item) {
        UNSAFE.putObject(this, itemOffset, item);
    }

    boolean casItem(E cmp, E val) {
        return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
    }

    void lazySetNext(Node<E> val) {
        UNSAFE.putOrderedObject(this, nextOffset, val);
    }

    boolean casNext(Node<E> cmp, Node<E> val) {
        return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
    }

    //....
}
private transient volatile Node<E> head;

可以看出Node()使用CAS保证原子性

源码分析

  • 入队操作源码

    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        // 入队前,创建一个入队节点
        Node<E> n = new Node<E>(e);
        retry:
        // 死循环,入队不成功反复入队。
        for (;;) {
            // 创建一个指向tail节点的引用
            Node<E> t = tail;
            // p用来表示队列的尾节点,默认情况下等于tail节点。
            Node<E> p = t;
            for (int hops = 0; ; hops++) {
                // 获得p节点的下一个节点。
                Node<E> next = succ(p);
                // next节点不为空,说明p不是尾节点,需要更新p后在将它指向next节点
                if (next != null) {
                    // 循环了两次及其以上,并且当前节点还是不等于尾节点
                    if (hops > HOPS && t != tail)
                    continue retry;
                    p = next;
                }
                // 如果p是尾节点,则设置p节点的next节点为入队节点。
                else if (p.casNext(null, n)) {
                    /*如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点,
                    更新失败了也没关系,因为失败了表示有其他线程成功更新了tail节点*/
                    if (hops >= HOPS)
                        casTail(t, n); // 更新tail节点,允许失败
                    return true;
                }
                // p有next节点,表示p的next节点是尾节点,则重新设置p节点
                else {
                    p = succ(p);
                }
            }
        }
    }
    

    分析: 使用循环加CAS传统操作保证线程安全

  • 定位尾节点

    tail节点并不总是尾节点,所以每次入队都必须先通过tail节点来找到尾节点!

    final Node<E> succ(Node<E> p) {
        Node<E> next = p.getNext();
        return (p == next) head : next;
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值