常用集合在多线程下的问题

常用集合在多线程下的问题,主要列举了collection接口,map接口及其主要实现类在多线程下的问题

hashset,无序,不可重复,判断插入的两个对象重复与否要看有没有重写实体的equals和hashcode方法

arraylist:有序,可重复,有讲解各种遍历删除在单线程下的正确与否,以及在多线程的问题下以及解决方案,

单线程下下面的写法会报异常:

Iterator<String>it = list.iterator();
		 while(it.hasNext()) {
		   String value = it.next();
		   //cursor的值为1,lastRet的值为0。注意此时,modCount为0,expectedModCount也为0。
		   if(value.equals("1")) {
			   list.remove(value);
			   //此时modCount为1,而expectedModCount为0
		   }
		 }
原因解析:修改次数和预期期望修改次数不一致,报错


这关系到arraylist的源码,arraylist的底层实现是数组

remove源码:

public E remove(int index) {
        rangeCheck(index);//有没有越界

        modCount++;//修改次数
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);//数组自删除,效率较高
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
itr源码:

 private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);//调用上面写的删除方法
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;//修改期望修改次数=修改次数
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();//抛出的是这里的异常
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {//检查是否可修改
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
单线程下解决方案:修改为:

Iterator<String>it = list.iterator();
		 while(it.hasNext()) {
		   String value = it.next();
		   if(value.equals("1")) {
			   it.remove();
		   }
		 }
但是,在多线程下他是会报异常的,解决方案:

使用线程安全的类:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>()

或者:

List<String> data = Collections.synchronizedList(new ArrayList<String>())

注意:这里的线程安全是指put add remove等是原子性操作,并不表示组合操作仍是线程安全的。


hashmap在多线程下的问题:

1.多线程扩容并且get的时候会死循环

2.put非空的元素get的时候为空

3.多线程put元素某些数据会丢失


hashmap多线程下死循环问题及解决方案:

这也关系到hashmap的底层实现结构是数据加链表,table[i]位置存放的是entry对象组成的链表

// 默认初始容量为16,必须为2的n次幂
     static final int DEFAULT_INITIAL_CAPACITY = 16 ;
 
     // 最大容量为2的30次方
     static final int MAXIMUM_CAPACITY = 1 << 30 ;
 
     // 默认加载因子为0.75f
     static final float DEFAULT_LOAD_FACTOR = 0 .75f;
 
     // Entry数组,长度必须为2的n次幂
     transient Entry[] table;
 
     // 已存储元素的数量
     transient int size ;
 
     // 下次扩容的临界值,size>=threshold就会扩容,threshold等于capacity*load factor
     int threshold;
 
     // 加载因子
     final float loadFactor ;

enrty类:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key ; 
        V value;
        Entry<K,V> next; // 指向下一个节点
        final int hash;
 
        Entry( int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
 
        public final K getKey() {
            return key ;
        }
 
        public final V getValue() {
            return value ;
        }
 
        public final V setValue(V newValue) {
           V oldValue = value;
            value = newValue;
            return oldValue;
        }
 
        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }
 
        public final int hashCode() {
            return (key ==null   ? 0 : key.hashCode()) ^
                   ( value==null ? 0 : value.hashCode());
        }
 
        public final String toString() {
            return getKey() + "=" + getValue();
        }
 
        // 当向HashMap中添加元素的时候调用这个方法,这里没有实现是供子类回调用
        void recordAccess(HashMap<K,V> m) {
        }
 
        // 当从HashMap中删除元素的时候调动这个方法 ,这里没有实现是供子类回调用
        void recordRemoval(HashMap<K,V> m) {
        }
}

hashmap的构造方法:
/**
     * 构造一个指定初始容量和加载因子的HashMap
     */
    public HashMap( int initialCapacity, float loadFactor) {
        // 初始容量和加载因子合法校验
        if (initialCapacity < 0)
            throw new IllegalArgumentException( "Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException( "Illegal load factor: " +
                                               loadFactor);
 
        // Find a power of 2 >= initialCapacity
        // 确保容量为2的n次幂,是capacity为大于initialCapacity的最小的2的n次幂
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
 
        // 赋值加载因子
        this.loadFactor = loadFactor;
        // 赋值扩容临界值
        threshold = (int)(capacity * loadFactor);
        // 初始化hash表
        table = new Entry[capacity];
        init();
    }
 
    /**
     * 构造一个指定初始容量的HashMap
     */
    public HashMap( int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
 
    /**
     * 构造一个使用默认初始容量(16)和默认加载因子(0.75)的HashMap
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }
 
    /**
     * 构造一个指定map的HashMap,所创建HashMap使用默认加载因子(0.75)和足以容纳指定map的初始容量。
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        // 确保最小初始容量为16,并保证可以容纳指定map
        this(Math.max(( int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY ), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
}

主要看put的实现:
public V put(K key, V value) {
        // 如果key为null,调用putForNullKey方法进行存储
        if (key == null)
            return putForNullKey(value);
        // 使用key的hashCode计算key对应的hash值
        int hash = hash(key.hashCode());
        // 通过key的hash值查找在数组中的index位置
        int i = indexFor(hash, table.length );
        // 取出数组index位置的链表,遍历链表找查看是有已经存在相同的key
        for (Entry<K,V> e = table [i]; e != null; e = e. next) {
            Object k;
            // 通过对比hash值、key判断是否已经存在相同的key
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                // 如果存在,取出当前key对应的value,供返回
                V oldValue = e. value;
                // 用新value替换之旧的value
                e. value = value;
                e.recordAccess( this);
                // 返回旧value,退出方法
                return oldValue;
            }
        }
 
        // 如果不存在相同的key
        // 修改版本+1
        modCount++;
        // 在数组i位置处添加一个新的链表节点
        addEntry(hash, key, value, i);
        // 没有相同key的情况,返回null
        return null;
    }
 
    private V putForNullKey(V value) {
        // 取出数组第1个位置(下标等于0)的节点,如果存在则覆盖不存在则新增,和上面的put一样不多讲,
        for (Entry<K,V> e = table [0]; e != null; e = e. next) {
            if (e.key == null) {
                V oldValue = e. value;
                e. value = value;
                e.recordAccess( this);
                return oldValue;
            }
        }
        modCount++;
        // 如果key等于null,则hash值等于0
        addEntry(0, null, value, 0);
        return null;
}

计算这个值在table中的位置和这个值的hashcode
 static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
 
    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
}

/**
     * 增加一个k-v,hash组成的节点在数组内,同时可能会进行数组扩容。
     */
    void addEntry( int hash, K key, V value, int bucketIndex) {
        // 下面两行行代码的逻辑是,创建一个新节点放到单向链表的头部,旧节点向后移
        // 取出索引bucketIndex位置处的链表节点,如果节点不存在那就是null,也就是说当数组该位置处还不曾存放过节点的时候,这个地方就是null,
       Entry<K,V> e = table[bucketIndex];
       // 创建一个节点,并放置在数组的bucketIndex索引位置处,并让新的节点的next指向原来的节点
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
       // 如果当前HashMap中的元素已经到达了临界值,则将容量扩大2倍,并将size计数+1
        if (size ++ >= threshold)
            resize(2 * table.length );
}

扩容的实现:
void resize( int newCapacity) {
        // 当前数组
        Entry[] oldTable = table;
        // 当前数组容量
        int oldCapacity = oldTable.length ;
        // 如果当前数组已经是默认最大容量MAXIMUM_CAPACITY ,则将临界值改为Integer.MAX_VALUE 返回
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        // 使用新的容量创建一个新的链表数组
        Entry[] newTable = new Entry[newCapacity];
        // 将当前数组中的元素都移动到新数组中
        transfer(newTable);
        // 将当前数组指向新创建的数组
        table = newTable;
        // 重新计算临界值
        threshold = (int)(newCapacity * loadFactor);
    }
 
    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable) {
        // 当前数组
        Entry[] src = table;
        // 新数组长度
        int newCapacity = newTable.length ;
        // 遍历当前数组的元素,重新计算每个元素所在数组位置
        for (int j = 0; j < src. length; j++) {
            // 取出数组中的链表第一个节点
            Entry<K,V> e = src[j];
            if (e != null) {
                // 将旧链表位置置空,因为这里出现的put非空数据get为空
                src[j] = null;
                // 循环链表,挨个将每个节点插入到新的数组位置中
                do {
                    // 取出链表中的当前节点的下一个节点
                    Entry<K,V> next = e. next;
                    // 重新计算该链表在数组中的索引位置
                    int i = indexFor(e. hash, newCapacity);
                    // 将下一个节点指向newTable[i]
                    e. next = newTable[i];
                    // 将当前节点放置在newTable[i]位置
                    newTable[i] = e;
                    // 下一次循环
                    e = next;
                } while (e != null);
            }
        }
}

删除:
/**
     * 根据key删除元素
     */
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e. value);
    }
 
    /**
     * 根据key删除链表节点
     */
    final Entry<K,V> removeEntryForKey(Object key) {
        // 计算key的hash值
        int hash = (key == null) ? 0 : hash(key.hashCode());
        // 根据hash值计算key在数组的索引位置
        int i = indexFor(hash, table.length );
        // 找到该索引出的第一个节点
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;
 
        // 遍历链表(从链表第一个节点开始next),找出相同的key,
        while (e != null) {
            Entry<K,V> next = e. next;
            Object k;
            // 如果hash值和key都相等,则认为相等
            if (e.hash == hash &&
                ((k = e. key) == key || (key != null && key.equals(k)))) {
                // 修改版本+1
                modCount++;
                // 计数器减1
                size--;
                // 如果第一个就是要删除的节点(第一个节点没有上一个节点,所以要分开判断)
                if (prev == e)
                    // 则将下一个节点放到table[i]位置(要删除的节点被覆盖)
                    table[i] = next;
                else
                 // 否则将上一个节点的next指向当要删除节点下一个(要删除节点被忽略,没有指向了)
                    prev. next = next;
                e.recordRemoval( this);
                // 返回删除的节点内容
                return e;
            }
            // 保存当前节点为下次循环的上一个节点
            prev = e;
            // 下次循环
            e = next;
        }
 
        return e;
}

get会遍历整个table[i]指向的链表,效率并不高
public V get(Object key) {
        // 如果key等于null,则调通getForNullKey方法
        if (key == null)
            return getForNullKey();
        // 计算key对应的hash值
        int hash = hash(key.hashCode());
        // 通过hash值找到key对应数组的索引位置,遍历该数组位置的链表
        for (Entry<K,V> e = table [indexFor (hash, table .length)];
             e != null;
             e = e. next) {
            Object k;
            // 如果hash值和key都相等,则认为相等
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                // 返回value
                return e.value ;
        }
        return null;
    }
 
    private V getForNullKey() {
        // 遍历数组第一个位置处的链表
        for (Entry<K,V> e = table [0]; e != null; e = e. next) {
            if (e.key == null)
                return e.value ;
        }
        return null;
}
是否包含的判断:
/**
     * Returns <tt>true</tt> if this map contains a mapping for the
     * specified key.
     *
     * @param   key   The key whose presence in this map is to be tested
     * @return <tt> true</tt> if this map contains a mapping for the specified
     * key.
     */
    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }
  
    /**
     * Returns the entry associated with the specified key in the
     * HashMap.  Returns null if the HashMap contains no mapping
     * for the key.
     */
    final Entry<K,V> getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        for (Entry<K,V> e = table [indexFor (hash, table .length)];
             e != null;
             e = e. next) {
            Object k;
            if (e.hash == hash &&
                ((k = e. key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
}

解决方案:

使用线程安全的ConcurrentHashMap

使用线程安全的hashtable,效率不高














  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值