ArrayList、HashMap、ConcurrentHashMap并发下出现的问题

一、 ArrayList

ArrayList不是线程安全的,因为没有加锁。在并发环境下,会出现一些问
。题。
1.add()方法会出现数组越界问题。
这里写图片描述
———先说说add()方法的内部实现,①step1先检查数组容量,②step2容量足够直接添加,容量不够扩容为原来1.5倍后添加。在说说为什么会出现数组越界问题,假设有两个线程在操作同一个ArrayList,线程一执行step1(容量足够)后被挂起,线程二执行add()方法后,线程一被唤醒,这时线程一因为已经不再判断容量是否足够(已经判断过),执行step2就会出现数组越界。
2.add()方法出现数据丢失和插入null值得问题。
假设初始size为0,elementData为空。
代码2分解为两步:

elementData[size] = e;
size ++;

当线程A执行完elementData[size] = e;还未执行size++就停止了,此时,size=0; elementData[0]放的是A的value,

然后线程B正常完成一个add操作;此时size=1,elementData[0]里面放的是B的value,把A的value给覆盖了。

然后B继续执行size++; 此时,size=2了

3.remove()方法出现删不干净得情况

 ArrayList<String> list = new ArrayList<String>();  
            list.add("a");  
            list.add("bb");  
            list.add("bb");  
            list.add("ccc");  
            list.add("ccc");  
            list.add("ccc"); 
-----------------------------------------------------
public static void remove(ArrayList<String> list) {  
    for (int i = 0; i < list.size(); i++) {  
        String s = list.get(i);  
        if (s.equals("bb")) {  
            list.remove(s);  
        }  
    }  
}             
对于以上写法,执行后会发现有一个“bb”的字符串没有删掉。

先看remove源码:

public boolean remove(Object o) {  
        if (o == null) {  
            for (int index = 0; index < size; index++)  
                if (elementData[index] == null) {  
                    fastRemove(index);  
                    return true;  
                }  
        } else {  
            for (int index = 0; index < size; index++)  
                if (o.equals(elementData[index])) {  
                    fastRemove(index);  
                    return true;  
                }  
        }  
        return false;  
    }  

按一般执行路径会走到else路径下最终调用fastRemove(index)方法;

private void fastRemove(int index) {  
    modCount++;  
    int numMoved = size - index - 1;  
    if (numMoved > 0)  
        System.arraycopy(elementData, index+1, elementData, index,  
                         numMoved);  
    elementData[--size] = null; // Let gc do its work  
}  

可以看到会执行System.arraycopy方法,导致删除元素时涉及到数组元素的移动。针对错误写法一,在遍历第二个元素字符串bb时因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动(也是字符串bb)至当前位置,导致下一次循环遍历时后一个字符串bb并没有遍历到,所以无法删除。针对这种情况可以倒序删除的方式来避免。
解决办法如下:

public static void remove(ArrayList<String> list){
    for(int i=list.size()-1;i>=0;i--){
        String s=list.get(i);
        if(s.equals("bb")){
            list.remove(s);
        }
    }
}

二、

HashMap

ps:jdk1.8版本中多线程put不会在出现死循环问题了,只有可能出现数据丢失的情况,因为1.8版本中,会将原来的链表结构保存在节点e中,然后依次遍历e,根据hash&n是否等于0,分成两条支链,保存在新数组中。
下面主要对1.7做出描述
1.put()方法在高并发下链表出现死循环,导致get()操作取不到数据。
先看源码

 //注意put方法是有返回值的
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //HashMap允许有一个key为null的键值对
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        //计算下标
        int i = indexFor(hash, table.length);
        //循环下标处的Entry链表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //hash值相等且key==或equals匹配,则替换value值并把旧值返回
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //若循环Entry链表未匹配到,则加一个Entry
        addEntry(hash, key, value, i);
        return null;
    }

    //新增一个Entry
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //若满足条件则需要扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }


    //扩容
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        //创建一个容量更大的新的数组
        Entry[] newTable = new Entry[newCapacity];
        //将旧数组中的Entry数据转移至新的数组
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }


    //转移数据
    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);
                //注意
                e.next = newTable[i];
                //注意
                newTable[i] = e;
                e = next;
            }
        }

Hashmap的Resize包含扩容和ReHash两个步骤,ReHash在并发的情况下可能会形成链表环。
当调用Get查找一个不存在的Key,而这个Key的Hash结果的位置恰好是带有环形链表的位置的时候,程序会进入死循环
上面的transfer方法,在多线程的情况下,会出现newTable[i].next=newTable[i]的情况,这也是文中提到的链表成环的原因

2.多线程put的时候可能导致元素丢失。
主要问题出在addEntry方法的new Entry (hash, key, value, e),如果两个线程都同时取得了e,则他们下一个元素都是e,然后赋值给table元素的时候有一个成功有一个丢失。

ConcurrentHashMap

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值