Java集合体系
2020-09-29 15:50:35   0  举报             
     
         
 AI智能生成
  Java集合体系脑图
    作者其他创作
 大纲/内容
  并发容器    
     CopyOnWriteArrayList    
     特性    
     通过可变数组实现  
     场景    
     List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突  
     线程安全  
     因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大  
     迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作  
     使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照  
     原理    
     动态数组机制    
     它内部有个“volatile数组”(array)来保持数据。
在“添加/修改/删除”数据时,都会新建一个数组,
并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”。
这就是它叫做CopyOnWriteArrayList的原因!CopyOnWriteArrayList就是通过这种方式实现的动态数组;
不过正由于它在“添加/修改/删除”数据时,都会新建数组,
所以涉及到修改数据的操作,CopyOnWriteArrayList效率很低;
但是单单只是进行遍历查找的话,效率比较高。  
     线程安全机制    
     是通过volatile和互斥锁来实现的。
(01) CopyOnWriteArrayList是通过“volatile数组”来保存数据的。
一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入;
就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的保证。
(02) CopyOnWriteArrayList通过互斥锁来保护数据。
在“添加/修改/删除”数据时,会先“获取互斥锁”,
再修改完毕之后,先将数据更新到“volatile数组”中,
然后再“释放互斥锁”;这样,就达到了保护数据的目的。   
     字段    
     ReentrantLock    
     主要对添加、修改、删除操作加锁  
     Volatile Object[]    
     存储数据,并保证了可见性  
     方法    
     get    
     获取数组的引用并根据索引获取数据,该过程不需要添加锁,
因为修改操作都是通过创建新数组(新内存)并赋予array数组,
但其旧的内存数据是没有改变的,所以获取方法不需要加锁,
但同事带来的问题是数据时效问题。  
     add    
     首先获取锁,然后通过Arrays.copyOf()方法创建新的数组,再把新数组的引用替换掉旧的array数组引用。  
     CopyOnWriteArraySet    
     通过CopyOnWriteArrayList实现  
     ConcurrentHashMap    
     通过锁的颗粒化实现,jdk1.7通过锁分段,jdk1.8通过在桶的首个元素节点加锁  
     key-value不能为空  
     jdk1.8    
     CAS + synchronized保证并发安全,底层通过数组+链表+红黑树实现  
     字段    
     table    
     默认为null,初始化发生在第一次插入操作,默认大小为16的数组,用来存储Node节点数据,扩容时大小总是2的幂次方  
     nextTable    
     默认为null,扩容时新生成的数组,其大小为原数组的两倍。  
     sizeCtl     
     默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。
-1 代表table正在初始化 
-N 表示有N-1个线程正在进行扩容操作 
其余情况: 
1、如果table未初始化,表示table需要初始化的大小。 
2、如果table初始化完成,表示table的容量,默认是table大小的0.75倍,居然用这个公式算0.75(n - (n >>> 2))。  
     Node    
     保存key,value及key的hash值的数据结构  
     ForwardingNode    
     一个特殊的Node节点,hash值为-1,其中存储nextTable的引用。
只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动  
     方法    
     put    
     通过CAS+synchronized实现并发插入或更新操作  
     1、若table为空,则先初始化table  
     2、通过key找到桶,如果当前桶为空时,通过CAS操作,将Node放入对应的bucket中  
     3、倘若当前map正在扩容f.hash == MOVED, 则跟其他线程一起进行扩容  
     4、则采用synchronized关键字。倘若当前hash对应的节点是链表的头节点,遍历链表,
若找到对应的node节点,则修改node节点的val,否则在链表末尾添加node节点;
倘若当前节点是红黑树的根节点,在树结构上遍历元素,更新或增加节点。  
     5、是否需要扩容  
     get    
     1. 空tab,直接返回null   
     2. 计算hash值,找到相应的bucket位置,为node节点直接返回,否则返回null  
     size    
     ConcurrentHashMap的元素个数等于baseCounter和数组里每个CounterCell的值之和,
这样做的原因是,当多个线程同时执行CAS修改baseCount值,失败的线程会将值放到CounterCell中。
所以统计元素个数时,要把baseCount和counterCells数组都考虑。  
     扩容    
     触发扩容    
     1、当前容量超过阈值  
     2、当链表中元素个数超过默认设定(8个),当数组的大小还未超过64的时候,此时进行数组的扩容,如果超过则将链表转化成红黑树  
     3、当发现其他线程扩容时,帮其扩容  
     扩容分析(transfer实现)    
     1、根据当前数组长度n,新建一个两倍长度的数组nextTable  
     2、初始化ForwardingNode节点(占位符),其中保存了新数组nextTable的引用,
当某个桶为空或列表元素迁移完成后设置该节点作为占位符,
表示该桶位已经处理过了其它线程帮助扩容时不再处理该桶了,转而处理其它桶  
     3、通过for自循环处理每个桶位中的链表元素    
     3.1、若桶位中链表为空,设置占位符(ForwardingNode),提醒其它线程该桶位已经迁移完数据  
     3.2、若桶位中存在链表,为首节点加锁并遍历链表将链表迁移到新的数组(nextTable)中,迁移完成后设置占位符(ForwardingNode)。提醒其它线程该桶位已经迁移完成  
     jdk1.7    
     同步机制    
     分段锁,每个segment继承ReentrantLock  
     存储结构    
     数组+链表  
     概念图            
     字段    
     Segment    
     锁分段数组,每个分段中包含数组+链表,分段对象继承自ReentrentLock  
     方法    
     put    
     1、扩容  
     2、通过key找到对应的锁段位  
     3、根据数组+链表结构添加数据  
     4、全程加锁  
     get    
     不需要加锁,  
     与HashMap的区别    
     ConcurrentHashMap是HashMap的高并发版本  
     ConcurrentHashMap是否能完全替代HashTable    
     1、Hashtable的任何操作都会把整个表锁住,是阻塞的。好处是总能获取最实时的更新  
     2、ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高  
     ConcurrentSkipListMap    
     通过跳表实现,有序,key-value不为空  
     数据结构    
     ConcurrentSkipListMap在原始链表的基础上增加了跳表的结构,所以需要两个额外的内部类来封装链表的节点,以及跳表的节点——Node和Index  
     同ConcurrentHashMap的Node节点一样,key为final,是不可变的,value和next通过volatile修饰保证内存可见性。  
     Index封装了跳表需要的结构,首先node包装了链表的节点,down指向下一层的节点(不是Node,而是Index),right指向同层右边的节点。
node和down都是final的,说明跳表的节点一旦创建,其中的值以及所处的层就不会发生变化(因为down不会变化,所以其下层的down都不会变化,那他的层显然不会变化)。 
Node和Index内部都提供了用于CAS原子更新的AtomicReferenceFieldUpdater对象,至于该对象的原理下面是不会深入研究的。  
     方法    
     put    
     1、通过方法findPredecessor找到待插入位置的前继节点和后继节点  
     2、判断链表结构是否被破坏,如果是则循环执行。  
     3、通过CAS更新链表  
     4、更新索引节点,首先生成level,然后向level及以下每一层级添加索引  
     get    
     从头索引开始向右查找并比较,否则向下索引,如果能匹配上则校验该过程中是否有其他线程修改了该值,没有则返回,否则重新获取。  
     ConcurrentSkipListSet    
     通过ConcurrentSkipListMap实现  
     ArrayBlockingQueue    
     数组实现的线程安全的有界的阻塞队列、元素不为空,因只有一个锁,索引增删改互斥  
     线程安全是指,ArrayBlockingQueue内部通过“互斥锁”保护竞争资源,实现了多线程对竞争资源的互斥访问。
而有界,则是指ArrayBlockingQueue对应的数组是有界限的。
 阻塞队列,是指多线程访问竞争资源时,当竞争资源已被某线程获取时,其它要获取该资源的线程需要阻塞等待;
而且,ArrayBlockingQueue是按 FIFO(先进先出)原则对元素进行排序,元素都是从尾部插入到队列,从头部开始返回。  
     原理    
     1、ArrayBlockingQueue继承于AbstractQueue,并且它实现了BlockingQueue接口。  
     2. ArrayBlockingQueue内部是通过Object[]数组保存数据的,也就是说ArrayBlockingQueue本质上是通过数组实现的。ArrayBlockingQueue的大小,即数组的容量是创建ArrayBlockingQueue时指定的。  
     3. ArrayBlockingQueue与ReentrantLock是组合关系,ArrayBlockingQueue中包含一个ReentrantLock对象(lock)。
ReentrantLock是可重入的互斥锁,ArrayBlockingQueue就是根据该互斥锁实现“多线程对竞争资源的互斥访问”。
而且,ReentrantLock分为公平锁和非公平锁,关于具体使用公平锁还是非公平锁,在创建ArrayBlockingQueue时可以指定;
而且,ArrayBlockingQueue默认会使用非公平锁。  
     4. ArrayBlockingQueue与Condition是组合关系,ArrayBlockingQueue中包含两个Condition对象(notEmpty和notFull)。
而且,Condition又依赖于ArrayBlockingQueue而存在,通过Condition可以实现对ArrayBlockingQueue的更精确的访问 --
 (01)若某线程(线程A)要取数据时,数组正好为空,则该线程会执行notEmpty.await()进行等待;
当其它某个线程(线程B)向数组中插入了数据之后,会调用notEmpty.signal()唤醒“notEmpty上的等待线程”。
此时,线程A会被唤醒从而得以继续运行。
 (02)若某线程(线程H)要插入数据时,数组已满,则该线程会执行notFull.await()进行等待;
当其它某个线程(线程I)取出数据之后,会调用notFull.signal()唤醒“notFull上的等待线程”。
此时,线程H就会被唤醒从而得以继续运行。  
     LinkedBlockingQueue    
     通过链表实现  
     LinkedBlockingQueue    
     1. LinkedBlockingQueue继承于AbstractQueue,它本质上是一个FIFO(先进先出)的队列。  
     2. LinkedBlockingQueue实现了BlockingQueue接口,它支持多线程并发。当多线程竞争同一个资源时,某线程获取到该资源之后,其它线程需要阻塞等待。  
     3. LinkedBlockingQueue是通过单链表实现的。
  (01) head是链表的表头。取出数据时,都是从表头head处取出。
(02) last是链表的表尾。新增数据时,都是从表尾last处插入。
(03) count是链表的实际大小,即当前链表中包含的节点个数。
(04) capacity是列表的容量,它是在创建链表时指定的。
(05) putLock是插入锁,takeLock是取出锁;notEmpty是“非空条件”,notFull是“未满条件”。
通过它们对链表进行并发控制。
LinkedBlockingQueue在实现“多线程对竞争资源的互斥访问”时,对于“插入”和“取出(删除)”操作分别使用了不同的锁。
对于插入操作,通过“插入锁putLock”进行同步;对于取出操作,通过“取出锁takeLock”进行同步。
此外,插入锁putLock和“非满条件notFull”相关联,取出锁takeLock和“非空条件notEmpty”相关联。通过notFull和notEmpty更细腻的控制锁。  
     LinkedBlockingDeque  
     ConcurrentLinkedQueue  
     集合辅助类    
     Collections    
     排序    
     自然排序或按照自定义比较器排序  
     搜索    
     二分搜索binarySearch  
     返回比较器  
     返回线程安全的结合    
     Collections.synchronizedxxx  
     Arrays    
     搜索    
     二分搜索binarySearch  
     判断    
     判断两个指定类型的数组是否相等  
     返回List    
     asList()  
     Collection接口    
     List接口(元素有序可重复)    
     子类    
     ArrayList    
     通过动态数组实现,每次扩容都是原来的3/2倍,数组是存储在一块连续的内存中  
     增删元素时,会创建新的数组,效率很低,不适合做大量的增删操作  
     可以通过索引的方式来访问元素,所以查找元素很方便  
     线程不安全  
     元素可为空且可重复  
     fail-fast    
     ConcurrentModificationException  
     默认容量:10  
     jdk1.6和jdk1.8的区别    
     主要区别在于1.6版本的在实例化时就把数组进行初始化了,而1.8版本的是在添加时进行初始化数组大小的  
     LinkedList    
     通过双向链表实现,头结点header,前指针,后指针,添加元素时在链表头添加,因为链表是通过指针方式查找元素故在内存中是不连续的内存块  
     双向循环链表,每一个元素都采用引用的方式来记住前后的元素  
     克服ArrayList增删元素效率低的问题而出现的,因为只需要改变引用即可,而数组则需要移动元素  
     线程不安全  
     元素可为空且可重复  
     fail-fast  
     Vector    
     通过数组实现  
     线程安全,通过关键字synchronize实现  
     元素可为空且可重复  
     相关知识    
     List中判断元素是否相等是通过元素的equals方法  
     Set接口(元素无序不可重复)    
     HashSet    
     通过HashMap实现,不是线程安全的,不可重复  
     LinkedHashSet  
     根据对象的哈希值来确定元素在集合中的位置,具有良好的存取和查找性能  
     不保证元素顺序,可为空  
     fail-fast  
     TreeSet    
     通过红黑树实现,不是线程安全的,有序集合  
     fail-fast  
     元素不能为空  
     Map接口    
     子类    
     HashMap    
     LinkedHashMap            
     通过数组+链表+双向链表,因为双向链表可以有序的,key-value可为空  
     继承自HashMap,get、put基本逻辑没变  
     同HashMap不同点在于LinkedHashMap维护了一个双向链表,  
     通过数组+链表实现,键和值都可以为空,线程不安全,无序  
     字段    
     初始容量    
     默认16  
     桶的数量  
     扩容容量为2的幂次,只有这样根据hashcode计算桶的下标时才能分布均匀,否则只能分布在奇数下标中  
     负载因子    
     默认0.75  
     桶自动增加之前可以达到多满的一种尺度  
     modCount    
     用来实现fail-fast机制的  
     fail-fast  
     方法    
     get    
     1、判断key是否为空,如果为空从table[0]中获取链表并遍历获取key。  
     2、根据key的hashcode计算桶的下标,获取桶的链表并遍历获取数据。  
     put    
     1、判断key是否为空,为空则将key-value存入第一个桶中,如果桶中已经存在该key,则更新。
如果不存在,则modCode++并且将key-value封装成Entry添加到链表头,并判断是否需要扩容。  
     2、根据key的hashcode计算出桶的下标,遍历链表,过程同1。  
     流程图            
     jdk1.7与jdk1.8不同    
     jdk1.8链表超过一定阈值后,采用红黑树结构  
     线程不安全    
     扩容不安全    
     jdk1.8之前存在,主要原因就是,采用头插法,新链表与旧链表的顺序是反的。
jdk1.8之后不存在,在1.8后采用尾插法就不会出现这种问题  
     死锁  
     其他  
     数组+链表,为啥要用链表?用来解决哈希冲突  
     为何HashMap的数组长度一定是2的次幂?    
     扩容时能保证得到的新的数组索引和老数组索引一致(大大减少了之前已经散列良好的老数组的数据位置重新调换),个人理解  
     数组长度保持2的次幂,length-1的低位都为1,会使得获得的数组索引index更加均匀  
     TreeMap    
     通过红黑树实现  
     有序的key-value集合  
     fail-fast  
     HashTable    
     通过数组+链表实现,key-value不能为空,线程安全  
     结构同HashMap类似  
     WeakHashMap    
     通过数组+链表实现,key-value可以为空  
     WeakHashMap的键是“弱键”。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。
更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,
这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了  
     弱键回收步骤    
     1、新建WeakHashMap,将“键值对”添加到WeakHashMap中。
实际上,WeakHashMap是通过数组table保存Entry(键值对);
每一个Entry实际上是一个单向链表,即Entry是键值对链表,Entry继承WeakReference。  
     2、当某“弱键”不再被其它对象引用,并被GC回收时。
在GC回收该“弱键”时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。  
     3、当下一次我们需要操作WeakHashMap时,会先同步table和queue。
table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,
就是删除table中被GC回收的键值对。  
     相关知识     
     将键映射到值的对象  
     一个映射不能包含重复的键,每个键最多映射到一个值  
    
 
 
 
 
  0 条评论
 下一页
  
   
   
   
   
  
  
  
  
  
  
  
  
  
  
 