diff --git a/blogs/Java/口水话/集合源码.md b/blogs/Java/口水话/集合源码.md index 1ef201b..d11754d 100644 --- a/blogs/Java/口水话/集合源码.md +++ b/blogs/Java/口水话/集合源码.md @@ -15,6 +15,7 @@ 9. TreeSet 10. CopyOnWriteArrayList 11. ConcurrentHashMap +12. SparseArray #### ArrayList @@ -109,5 +110,25 @@ CopyOnWriteArrayList 读的时候不需要加锁,适合读多写少的场景 #### ConcurrentHashMap -ConcurrentHashMap 是一个线程安全的 HashMap,它是不允许 key 或 value 为 null 的。它的底层也是使用数组 + 链表 + 红黑树来实现的, +ConcurrentHashMap 和 HashMap 相比,它的底层也是使用数组 + 链表 + 红黑树来实现的,不过新增了转移节点(ForwardingNode),是为了保证扩容时的线程安全的节点。而且红黑树结构略有不同,HashMap 的红黑树节点叫做 TreeNode,TreeNode 不仅仅有属性,是维护者红黑树的结构,比如说查找、新增等;ConcurrentHashMao 中红黑树被拆分成两块,TreeNode 仅仅维护属性和查找功能,新增了 TreeBin,来维护红黑树结构,并负责根节点的加锁和解锁。 + +和 HashTable 相比,它也是线程安全的,但是 ConcurrentHashMap 多个线程同时进行 put、remove 等操作时并不会阻塞,可以同时进行;HashTable 在操作时,会锁住整个 Map。 + +ConcurrentHashMap 在 put 时,在一个 for 死循环里面,也就是一定能保证 put 成功。第一次 put 时,会先通过 CAS 保证只有一个线程扩容,如果有其他线程在执行扩容操作,就会调用 Thread.yield 释放当前 CPU 调度权,重新发起锁的竞争,这一步是在一个 while 循环里面去做的只要容量为空,就一直循环。再求出 table 索引之后,如果该槽点为空并不是直接新增,而是通过 CAS 新增。如果判断当前是转移节点(转转移节点的 hash 值固定为 MOVED 为 -1),表示该槽点正在扩容,就会一直等待扩容完成;如果当前槽点有值,就是 key 的 hash 冲突的情况,此时槽点上可能是链表或红黑树,这时候就会通过 synchronized 锁住槽点,来保证同一时刻只会有一个线程能对槽点进行修改。也就是说,put 是通过自旋 + CAS + 锁来实现线程安全的。 + +在进行扩容时,首先需要把老数组的值全部拷贝到新数组上,先从数组的队尾开始拷贝,拷贝数组的槽点时,会先把原数组的槽点,保证原数组槽点不能操作,成功拷贝到新数组时,把原数组槽点赋值为转移节点。如果此时有数据要 put 到此槽点就会一直等待。拷贝时也是扩容原数组两倍大小。 + +ConcurrentHashMap 的 get 和 HashMap 基本无差。 + +#### SparseArray + +SparseArray 是 Android 中一种特有的数据结构,用来替代 HashMap 的。它里面有两个数组,一个是 int[] 数组存放 key,一个是 Object[] value 数组。也就是它的 key 只能为 int;在 put 时,会根据传入的 key 进行二分查找找到合适的插入位置,如果当前位置有值或者是 DELETED 节点,就直接覆盖,否则就需要拷贝数组后移一位,空出一个位置让其插入。如果数组满了但是还有 DELETED 节点,就需要调用 gc 方法,gc 方法所做的就是把 DELETED 节点后面的数前移,也就是真正的把 DELETED 节点删掉然后在插入,否则就只能扩容了。 + +调用 remove 时,并不会直接把 key 从 int[] 数组里面删掉,而是把当前 key 指向的 value 设置成 DELETED 节点,这样做是为了减少 int[] 数组的结构调整,结构调整就意味着数据拷贝。但是当我们调用 keyAt/valueAt 获取索引时,如果有 DELETED 节点就必须得调用 gc,不然获得的 index 肯定不对。 + +get 方法就比较简单了,二分查找获取 key 对应的索引 index,返回 values[index] 即可。 + +可以看到,SparseArray 比 HashMap 少了基本数据的自动装箱操作,而且不需要额外的结构体,单个元素存储成本低,在数据量小的情况下,随机访问的效率很高。但是缺点也是显而易见的,就是增删的效率比较低,在数据量比较大的时候,调用 gc 拷贝数组成本巨大。 + +除了 SparseArray,Android 还提供了 SparseIntArray(int:int)、SparseBooleanArray(int:boolean)、SparseLongArray(int:long) 等,其实就是把对应的 value 换成基本数据类型。