Java线程集合类ConcurrentHashMap&CopyOnWriteArrayList 深入

时间:2022-01-20 18:00:42
一:(阻塞队列)BlockingQueue(子类:常用LinkedBlockingQueue和ArrayBlockingQueue) private BlockingQueue<MapMonitorEntry> entryQueue = new LinkedBlockingQueue<MapMonitorEntry>(); put( ) take( ) --阻塞方法,put当队列数据满会阻塞,take当队列空阻塞等待数据 offer       添加一个元素并返回true       如果队列已满,则返回false
poll         移除并返问队列头部的元素    如果队列为空,则返回null,TimeUnits等待时间后返回
peek       返回队列头部的元素             如果队列为空,则返回null
put    添加一个元素                 如果队列满,则阻塞
take   移除并返回队列头部的元素     如果队列为空,则阻塞
1)LinkedBlockingQueue:*,生产者消费者是独立锁进行控制真正并行,插入或删除产生额外Node对象占内存堆 2)ArrayBlockingQueue:有界,共用锁并非真正并行,插入或删除不产生额外对象
tip:为什么需要rehash?

随着key不断的添加,bucket下的单链表越来越长,查找、删除效率越来越低,需要对ht进行expand,增加bucket(数组长度)个数,让链表的长度减少。bucket数量的增多,原有bucket的key需要迁移到新的bucket上,于是有了rehash的这个过程.


二:(并发容器类)ConcurrentHashMap 

更具细粒度的锁,更高效,在segment上引入锁概念,32个段。HashTable是集合锁。但是 size() 和 containValues 方法是锁定整个表,而不是段。

细粒度锁,分离锁,较HashMap,高并发情景下(50-100线程并发)添加和删除性能提高一倍,查找性能提高10倍

HashTable:锁整个hash表,ConcurrentHashMap废除对整个hash表锁操作,转而在每个bucket加锁,锁粒度细化

ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。(ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的 final修饰,value是volatile保证看到最新)。


get读不需要锁volatile V value(保证立可见)

size:一个Segment中的有一个modCount变量,代表的是对Segment中元素的数量造成影响的操作的次数,这个值只增不减,size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,则把这个过程再重复做一次,如果再不相同,则就需要将所有的Segment都锁住,然后一个一个遍历。

Segment:每个段继承ReentrantReadWriteLock锁,每个段自带读写锁remove,put操作

count每个put,remove操作都会更新count值,所以易变的volatile transient volatile int count;

modcount:只增不减值,只是为比较集合是否有变动,如果变动所有segment加锁,否则直接取值求和

迭代写:遍历同时写操作不会发生ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。

remove:HashEntry中的next是final的,一经赋值以后就不可修改,在定位到待删除元素的位置以后,程序就将待删除元素前面的那一些元素全部复制一遍,然后再一个一个重新删除前:删除后: 

containsValue

    ConcurrentHashMap.containsValue(Object value):

   for (int i = 0; i < segments.length; ++i) {

                int c = segments[i].count;

                mcsum += mc[i] = segments[i].modCount;

                if (segments[i].containsValue(value))//遍历所有段,每段都要锁

                    return true;

}

ConcurrentHashMap$.containsValue(Object value):

boolean containsValue(Object value) {

            readLock().lock();//读锁

            try {

                if (count != 0) { // read-volatile

                    HashEntry<K,V>[] tab = table;

                    int len = tab.length;

                    for (int i = 0 ; i < len; i++) {

                        for (HashEntry<K,V> e = tab[i]; e != null; e = e.next) {

                            V v = e.value;

                            if (value.equals(v))

                                return true;

                        }

                    }

                }

                return false;

            } finally {

                readLock().unlock();

            }

        }

static final class HashEntry<K,V> {  

    final K key;  

    final int hash;  

    volatile V value;  

    final HashEntry<K,V> next;//后续节点删除,不能重新赋值链接,之前节点重新复制一遍  

}


三:synchronizedMap 和 synchronizedList(有条件线程安全)

     单个操作是线程安全的,相当于对原集合每个操作方法加集合的对象锁,但是多个操纵组成的操纵序列却可能导致数据争用,由于在操纵序列中控制流取决于前面操纵的结果

     多个操作size put get组合外面必须加该集合对象锁,容易发生ConcurrentModifyException

四:CopyOnWriteArrayList 与 CopyOnWriteArraySet

   (“写入时复制”策略)基本思想是一旦对容器有修改,那么就“复制”一份新的集合,在新的集合上修改,然后将新集合复制给旧的引用。当然了这部分少不了要加锁。读取不需要加锁 volatile

  1. List仍然是基于数组的实现,因为只有数组是最快的。
  2. 为了保证无锁的读操作能够看到写操作的变化,因此数组array是volatile类型的。
  3. get/indexOf/iterator等操作都是无锁的加volatile即可,同时也可以看到所操作的都是某一时刻array的镜像(这得益于数组是不可变化的)
  4. add/set/remove/clear等元素变化的都是需要加锁的,这里使用的是ReentrantLock。 
private volatile transient Object[] array;//volatile易变量修改后其它线程立即可见,transient非序列化(非持久字段)

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        Object oldValue = elements[index];
        if (oldValue != element) {
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len);
        newElements[index] = element;
        setArray(newElements);
        } else {
        // Not quite a no-op; ensures volatile write semantics
        setArray(elements);
        }
        return (E)oldValue;
    } finally {
        lock.unlock();
    }
    }

ConcurrentModifyException
在遍历collection和set时,不要remove其中的元素。如果remove的话,先iterate一下,再使用iterrate的remove()方法。如下所示:for(Iterate it=Correction.iterate();it.hasNext();){       XX=it.next();       if(......)        {               it.remove();        }}或者Collection<User> users =new CopyOnWriteArrayList();//如果new ArrayList() 会 CollectionModifyExceptionusers.add(new User("张三",28));users.add(new User("李四",25));users.add(new User("王五",31));for(User user:users){ users.remove(user);}System.out.println(users.size());

三:TimeUnit TimeUnit.SECONDS.sleep(10);   四:CopyOnWriteArrayList

是线程安全的List实现,其底层数据存储结构为数组(Object[] array),它在读操作远远多于写操作的场景下表现良好,这其中的原因在于其读操作(get(),indexOf(),isEmpty(),contains())不加任何锁,而写操作(set(),add(),remove())通过Arrays.copyOf()操作拷贝当前底层数据结构(array),在其上面做完增删改等操作,再将新的数组置为底层数据结构,同时为了避免并发增删改, CopyOnWriteList在这些写操作上通过一个ReetranLock进行并发控制。

线程安全的,一个读,一个写复制数据后,读写针对不同对象,当写完后,将对象引用重新赋值,保持最新