From c2448ff0bda4d236ca422f4b2f72fbfe9057c361 Mon Sep 17 00:00:00 2001 From: Omooo <869759698@qq.com> Date: Mon, 15 Jun 2020 21:37:11 +0800 Subject: [PATCH] =?UTF-8?q?Update=20=E9=9B=86=E5=90=88=E6=BA=90=E7=A0=81.m?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blogs/Java/口水话/集合源码.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/blogs/Java/口水话/集合源码.md b/blogs/Java/口水话/集合源码.md index 0dafc59..222b023 100644 --- a/blogs/Java/口水话/集合源码.md +++ b/blogs/Java/口水话/集合源码.md @@ -2,7 +2,11 @@ 集合源码口水话 --- -目录 +#### 前言 + + + +#### 目录 1. ArrayList 2. Vector @@ -65,7 +69,7 @@ HashMap 非线程安全,如果需要满足线程安全,可以用 Collections 同时,取 hash 的时候是通过 hashCode 的高十六位和低十六位异或得到,这样做在数组长度比较小的时候也能保证高位 bit 都能参与到 hash 计算中,同时不会有太大开销。 -然后再讲一下扩容机制,扩容的时候是容量翻倍,也就是 x2,这也同时保证了长度依旧是 2 的次方,所以前面基于位运算取索引的优化得以保留。既然数组长度改变了,那么肯定需要重新计算索引位置呀?这里又有一个优化点,当 n 为 2 次方时,x2 不过是在高位补 1,然后在进行与运算,与 1 进行与运算就是它本身嘛。所以这时只需要看 hash 的高位是 1 还是 0,如果是 0,索引不变,如果是 1,新索引就是原索引加旧桶值。这也就避免了重新计算索引,只需要看 hash 的高位是 1 还是 0 即可。但是你可能会说,这必须得保证数组长度为 n 次方呀,我们可以在初始化 HashMap 时传一个非 2 的 n 次方的数,这就炸了。其实呢,HashMap 会根据你传的舒适容量,自动调节到 2 的 n 次方上,比如传 15 就是 16,传 17 呢就是 32,向上转一个最接近的 2 次幂数。 +然后再讲一下扩容机制,扩容的时候是容量翻倍,也就是 x2,这也同时保证了长度依旧是 2 的次方,所以前面基于位运算取索引的优化得以保留。既然数组长度改变了,那么肯定需要重新计算索引位置呀?这里又有一个优化点,当 n 为 2 次方时,x2 不过是在高位补 1,然后在进行与运算,与 1 进行与运算就是它本身嘛。所以这时只需要看 hash 的高位是 1 还是 0,如果是 0,索引不变,如果是 1,新索引就是原索引加旧桶值。这也就避免了重新计算索引,只需要看 hash 的高位是 1 还是 0 即可。但是你可能会说,这必须得保证数组长度为 n 次方呀,我们可以在初始化 HashMap 时传一个非 2 的 n 次方的数,这就炸了。其实呢,HashMap 会根据你数组容量,自动调节到 2 的 n 次方上,比如传 15 就是 16,传 17 呢就是 32,向上转一个最接近的 2 次幂数。 最后,在这里面我并没有将 HashMap 的具体 put/remove/get 的实现,这些其实就是数组或链表或红黑树的操作,数组和链表大家都很熟悉了,红黑树我也不是很懂,只需要记得每次 put/remove 时,都会进行着色和旋转,使得红黑树更加平衡。再不济就把红黑树看成一颗二叉搜索树也行趴。 @@ -79,7 +83,7 @@ Hashtable 在扩容时,是 x2 + 1 的。Hashtable 在处理 hash冲突时, TreeMap 底层的数据结构就是红黑树,和 HashMap 的红黑树结构一样。不同的是,TreeMap 通过 compare 来比较 key 的大小,然后利用红黑树左小右大的特性,为每个 key 找到自己的位置,维护了 key 的大小关系,适用于 key 需要排序的场景。 -因为底层使用的是红黑树的结构,所以它的 put/remove/get 等方法实际复杂度都是 log(n) 的。 +因为底层使用的是红黑树的结构,所以它的 put/remove/get 等方法时间复杂度都是 log(n) 的。 #### LinkedHashMap @@ -91,7 +95,7 @@ LinkedHashMap 本身是继承 HashMap 的,所以它拥有 HashMap 的所有特 但是呢,LinkedHashMap 只提供了单向访问,即按照插入的顺序从头到尾进行访问,不能像 LinkedList 那样可以双向访问。 -在使用 LRU 策略时,可以覆写删除策略的方法 removeEldestEntry 方法,比如可以指定当节点数大于 10 就开始头结点(size() > 10)等等。 +在使用 LRU 策略时,可以覆写删除策略的方法 removeEldestEntry 方法,比如可以指定当节点数大于 10 就开始删除头结点(size() > 10)等等。 #### HashSet @@ -120,13 +124,13 @@ CopyOnWriteArrayList 读的时候不需要加锁,适合读多写少的场景 #### ConcurrentHashMap -ConcurrentHashMap 和 HashMap 相比,它的底层也是使用数组 + 链表 + 红黑树来实现的,不过新增了转移节点(ForwardingNode),是为了保证扩容时的线程安全的节点。而且红黑树结构略有不同,HashMap 的红黑树节点叫做 TreeNode,TreeNode 不仅仅有属性,是维护者红黑树的结构,比如说查找、新增等;ConcurrentHashMao 中红黑树被拆分成两块,TreeNode 仅仅维护属性和查找功能,新增了 TreeBin,来维护红黑树结构,并负责根节点的加锁和解锁。 +ConcurrentHashMap 和 HashMap 相比,它的底层也是使用数组 + 链表 + 红黑树来实现的,不过新增了转移节点(ForwardingNode),是为了保证扩容时的线程安全的节点。而且红黑树结构略有不同,HashMap 的红黑树节点叫做 TreeNode,TreeNode 不仅仅有属性,还维护着红黑树的结构,比如说查找、新增等;ConcurrentHashMap 中红黑树被拆分成两块,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 + 锁来实现线程安全的。 +ConcurrentHashMap 在 put 时,在一个 for 死循环里面,也就是一定能保证 put 成功。第一次 put 时,会先通过 CAS 保证只有一个线程扩容,如果有其他线程在执行扩容操作,就会调用 Thread.yield 释放当前 CPU 调度权,重新发起锁的竞争,这一步是在一个 while 循环里面去做的只要容量为空,就一直循环。再求出 table 索引之后,如果该槽点为空并不是直接新增,而是通过 CAS 新增。如果判断当前是转移节点(转移节点的 hash 值固定为 MOVED 为 -1),表示该槽点正在扩容,就会一直等待扩容完成;如果当前槽点有值,就是 key 的 hash 冲突的情况,此时槽点上可能是链表或红黑树,这时候就会通过 synchronized 锁住槽点,来保证同一时刻只会有一个线程能对槽点进行修改。也就是说,put 是通过自旋 + CAS + 锁来实现线程安全的。 -在进行扩容时,首先需要把老数组的值全部拷贝到新数组上,先从数组的队尾开始拷贝,拷贝数组的槽点时,会先把原数组的槽点,保证原数组槽点不能操作,成功拷贝到新数组时,把原数组槽点赋值为转移节点。如果此时有数据要 put 到此槽点就会一直等待。拷贝时也是扩容原数组两倍大小。 +在进行扩容时,首先需要把老数组的值全部拷贝到新数组上,先从数组的队尾开始拷贝,拷贝数组的槽点时,会先把原数组的槽点锁住,保证原数组槽点不能操作,成功拷贝到新数组时,把原数组槽点赋值为转移节点。如果此时有数据要 put 到此槽点就会一直等待。拷贝时也是扩容原数组两倍大小。 ConcurrentHashMap 的 get 和 HashMap 基本无差。 @@ -148,7 +152,7 @@ LinkedBlockingQueue 主要用在线程池中,当我们使用 Executors 的 new SynchronousQueue 是一个不存储元素的阻塞队列,每一个 put 操作必须等待 take 操作,否则不能添加元素。在使用 Executors 的 newCachedThreadPool 创建线程池用的就是它,在 AsyncTask 创建的一个单线程池用的也是它,核心线程数为 1,最大线程数为 20,非核心线程空闲存活时间为三秒,适合任务多但是执行比较快的场景中。 -源水太深了,还没看明白,待定。 +源码水太深了,还没看明白,待定。 #### SparseArray @@ -174,4 +178,4 @@ ArrayMap 在 put/remove 时,和 SparseArray 基本一致,也是通过二分 除了 put 方法,ArrayMap 和 SparseArray 都有一个 append 方法,它和 put 很相似,append 的差异在于该方法不会去做扩容操作,是一个轻量级的插入方法。在明确知道肯定会插入队尾的情况下使用 append 性更好,因为 put 一上来就做二分查找,时间复杂度 O(logn),而 append 时间复杂度为 O(1)。 -ArraySet 也是 Android 特有的数据结构,用来替代 HashSet 的,和 ArrayMap 几乎一致,包含了缓存机制、扩容机制。 \ No newline at end of file +ArraySet 也是 Android 特有的数据结构,用来替代 HashSet 的,和 ArrayMap 几乎一致,包含了缓存机制、扩容机制等。 \ No newline at end of file