JUC学习笔记 -- (3)同步容器类和并发容器类

一、同步容器类

包括Vector(实现了一个动态数组,和ArrayList相似,但两者是不同的)和Hashtable。

同步容器类的问题:

例如:Vector的getLast方法,和deleteLast方法,都会执行“先检查,后执行”操作,每个方法首先都获得数组的大小,然后通过结果来获取或删除最后一个元素。如果线程A调用getLast,线程B调用deleteLast,在线程A调用size和getLast之间,线程B删除了一个元素,则getLast会出现数组越界异常。同样,这种风险在对Vector中的元素进行迭代时仍然会出现。

解决方法:在客户端,将每个方法都进行加锁,使得每次只能有一个线程访问容器。

缺点:这样的话,同步容器类将所有对容器状态的访问都串行化,以实现它们的线程安全性,代价是严重降低并发性。

1、Hashtable

链表+数组实现,初始容量11,扩容因子0.75。每一次扩容,为上一次容量的2倍+1.




源码:

  // 返回key对应的value,没有的话返回null
    public synchronized V get(Object key) {
        Entry tab[] = table;
        int hash = key.hashCode();
        // 计算索引值,
        int index = (hash & 0x7FFFFFFF) % tab.length;
        // 找到“key对应的Entry(链表)”,然后在链表中找出“哈希值”和“键值”与key都相等的元素
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return e.value;
            }
        }
        return null;
    }

// 将“key-value”添加到Hashtable中
    public synchronized V put(K key, V value) {
        // Hashtable中不能插入value为null的元素!!!
        if (value == null) {
            throw new NullPointerException();
        }


        // 若“Hashtable中已存在键为key的键值对”,
        // 则用“新的value”替换“旧的value”
        Entry tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                V old = e.value;
                e.value = value;
                return old;
                }
        }


        // 若“Hashtable中不存在键为key的键值对”,
        // (01) 将“修改统计数”+1
        modCount++;
        // (02) 若“Hashtable实际容量” > “阈值”(阈值=总的容量 * 加载因子)
        //  则调整Hashtable的大小
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();


            tab = table;
            index = (hash & 0x7FFFFFFF) % tab.length;
        }


        // (03) 将“Hashtable中index”位置的Entry(链表)保存到e中
        Entry<K,V> e = tab[index];
        // (04) 创建“新的Entry节点”,并将“新的Entry”插入“Hashtable的index位置”,并设置e为“新的Entry”的下一个元素(即“新Entry”为链表表头)。        
        tab[index] = new Entry<K,V>(hash, key, value, e);
        // (05) 将“Hashtable的实际容量”+1
        count++;
        return null;
    }

 // 删除Hashtable中键为key的元素
    public synchronized V remove(Object key) {
        Entry tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        // 找到“key对应的Entry(链表)”
        // 然后在链表中找出要删除的节点,并删除该节点。
        for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
                if (prev != null) {
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;
                }
                count--;
                V oldValue = e.value;
                e.value = null;
                return oldValue;
            }
        }
        return null;
    }

 

注:Hashtable继承Dictionary类,实现map、cloneable、serializable接口。

      Hashtable键为null,则抛出NullPointException,它的clear方法就是将所有的数组元素都置为null。

2、Vector

数组实现,初始容量10。Vector 当扩容容量增量大于0时、新数组长度为原数组长度+扩容容量增量、否则新数组长度为原数组长度的2倍。与ArrayList不同的就是两方面,分别是是否线程安全和扩容机制。

二、并发容器类(针对多个线程并发访问设计的)

包括ConcurrentHashMap、CopyOnWriteArrayList

1、ConcurrentHashMap

并不是在每个方法上都加锁,使得每次只能有一个线程访问容器,而是使用分段锁。

ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护散列桶的1/16,其中第N个散列桶由第(Nmod16)个锁来保护,正是这项技术使得ConcurrentHashMap能够支持多达16个并发的写入器。

锁分段的劣势:当需要加锁整个容器的时候,单个锁来实现独占访问只需要获得一个锁,ConcurrentHashMap需要获得16个锁,增加了复杂度。

ConcurrentHashMap源码解析:

 

ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。

采用二次哈希的方法:第一次定位到哪一个segment,第二次定位到具体的桶。

进行读操作时,不需要加锁:首先判断count(该segment桶的数量)是否为0,若为0,则返回null,否则,根据hash值和key找到相应的value,判断value是否为null,若为null,则利用加锁的方式再次去获取,因为此时锁正被写操作所占有,读操作需要等待写操作释放锁后才能再去获取(防止在此期间新增了一个entry,另一个线程新增的这个entry又恰好是我们要get的,因为这个entry可能只构造了一半),否则返回该值(即要查找的值)。

注:count和value都是volatile类型,所以假设有线程增加或删除一个entry,count的新值都是对所有线程可见的;如果get过程中,另一个线程改变了该value值,对于该线程来说也是可见的。若在此期间删除(next是不可变的,所以需要重新复制一份链表以实现相应的删除操作)了要查找的entry,而get查找的仍然是旧链表,所以仍然会找到value并返回。

进行增加、删除、修改操作时都需要加锁。

在容器的size()操作时,需要依次加锁整个容器,然后再按顺序释放各个锁,一定要按顺序,否则可能会造成死锁.

注:ConcurrentHashMap是弱一致性的,解释如下:

什么是弱一致性,我举个简单的例子,例如题主说的clear方法!
因为没有全局的锁,在清除完一个segments之后,正在清理下一个segments的时候,已经清理segments可能又被加入了数据,因此clear返回的时候,ConcurrentHashMap中是可能存在数据的。因此,clear方法是弱一致的。
!!总结!!
ConcurrentHashMap的弱一致性主要是为了提升效率,但是成为弱一致。
Hashtable为了线程安全的强一致性,就需要全局锁,降低效率。
一致性与效率之间的一种权衡选择关系

2、CopyOnWriteArrayList

当修改(包括更新、添加、删除)CopyOnWriteArrayList时,需要加锁(可重入锁),先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,将原来数组的引用指向新数组。

在进行读操作的时候,如果写操作尚未完成,则读的是旧容器中的数据。所以,多个线程可以同时对这个容器进行迭代,而不会彼此干扰或者与修改线程的容器相互干扰。

缺点:(1)内存占用问题:每当修改容器时,都会复制底层数组,需要占用内存。

    (2)数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWriteArrayList容器。

    原文作者:JUC
    原文地址: https://blog.csdn.net/abc997995674/article/details/83269428
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞