主要内容:Kotlin, Java, RxJava, 多线程/并发, 集合
LruCache 用来实现基于内存的缓存,LRU 就是最近最少使用的意思,LruCache 基于 LinkedHashMap 实现。LinkedHashMap 是在 HashMap 的基础之上进行了封装,除了具有哈希功能,还将数据插入到双向链表中维护。每次读取的数据会被移动到链表的尾部,当达到了缓存的最大的容量的时候就将链表的首部移出。使用 LruCache 的时候需要注意的是单位的问题,因为该 API 并不清楚要存储的数据是如何计算大小的,所以它提供了方法供我们实现大小的计算方式。(《Android 内存缓存框架 LruCache 的源码分析》)
DiskLruCache 与 LruCache 类似,也是用来实现缓存的,并且也是基于 LinkedHashMap 实现的。不同的是,它是基于磁盘缓存的,LruCache 是基于内存缓存的。所以,LinkedHashMap 能够存储的空间更大,但是读写的速率也更慢。使用 DiskLruCache 的时候需要到 Github 上面去下载。OkHttp 和 Glide 的磁盘缓存都是基于 DiskLruCache 开发的。DiskLruCahce 内部维护了一个日志文件,记录了读写的记录的信息。其他的基本都是基础的磁盘 IO 操作。
如果想线程安全地使用这列表类(可以参考下面的问题)
我们有几种方式可以线程间安全地操作 List. 具体使用哪种方式,可以根据具体的业务逻辑进行选择。通常有以下几种方式:
sychronized
进行控制。我们可以在我们自己的业务方法上面进行加锁来保证线程安全。Collections.synchronizedList()
进行包装。这个方法内部使用了私有锁来实现线程安全,就是通过对一个全局变量进行加锁。调用我们的 List 的方法之前需要先获取该私有锁。私有锁可以降低锁粒度。CopyOnWriteArrayList
代替 ArrayList,使用 ConcurrentLinkedQueue
代替 LinkedList. 并发容器中的 CopyOnWriteArrayList
在读的时候不加锁,写的时候使用 Lock 加锁。ConcurrentLinkedQueue
则是基于 CAS 的思想,在增删数据之前会先进行比较。SparseArray 主要用来替换 Java 中的 HashMap,因为 HashMap 将整数类型的键默认装箱成 Integer (效率比较低). 而 SparseArray 通过内部维护两个数组来进行映射,并且使用二分查找寻找指定的键,所以它的键对应的数组无需是包装类型。SparseArray 用于当 HashMap 的键是 Integer 的情况,它会在内部维护一个 int 类型的数组来存储键。同理,还有 LongSparseArray, BooleanSparseArray 等,都是用来通过减少装箱操作来节省内存空间的。但是,因为它内部使用二分查找寻找键,所以其效率不如 HashMap 高,所以当要存储的键值对的数量比较大的时候,考虑使用 HashMap.
HashMap (下称 HM) 是哈希表,ConcurrentHashMap (下称 CHM) 也是哈希表,它们之间的区别是 HM 不是线程安全的,CHM 线程安全,并且对锁进行了优化。对应 HM 的还有 HashTable (下称 HT),它通过对内部的每个方法加锁来实现线程安全,效率较低。
HashMap 的实现原理:HashMap 使用拉链法来解决哈希冲突,即当两个元素的哈希值相等的时候,它们会被方进一个桶当中。当一个桶中的数据量比较多的时候,此时 HashMap 会采取两个措施,要么扩容,要么将桶中元素的数据结构从链表转换成红黑树。因此存在几个常量会决定 HashMap 的表现。在默认的情况下,当 HashMap 中的已经被占用的桶的数量达到了 3/4 的时候,会对 HashMap 进行扩容。当一个桶中的元素的数量达到了 8 个的时候,如果桶的数量达到了 64 个,那么会将该桶中的元素的数据结构从链表转换成红黑树。如果桶的数量还没有达到 64 个,那么此时会对 HashMap 进行扩容,而不是转换数据结构。
从数据结构上,HashMap 中的桶中的元素的数据结构从链表转换成红黑树的时候,仍然可以保留其链表关系。因为 HashMap 中的 TreeNode 继承了 LinkedHashMap 中的 Entry,因此它存在两种数据结构。
HashMap 在实现的时候对性能进行了很多的优化,比如使用截取后面几位而不是取余的方式计算元素在数组中的索引。使用哈希值的高 16 位与低 16 进行异或运算来提升哈希值的随机性。
因为每个桶的元素的数据结构有两种可能,因此,当对 HashMap 进行增删该查的时候都会根据结点的类型分成两种情况来进行处理。当数据结构是链表的时候处理起来都非常容易,使用一个循环对链表进行遍历即可。当数据结构是红黑树的时候处理起来比较复杂。红黑树的查找可以沿用二叉树的查找的逻辑。
下面是 HashMap 的插入的逻辑,所有的插入操作最终都会调用到内部的 putVal()
方法来最终完成。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
private V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) {
// 原来的数组不存在
n = (tab = resize()).length;
}
i = (n - 1) & hash; // 取哈希码的后 n-1 位,以得到桶的索引
p = tab[i]; // 找到桶
if (p == null) {
// 如果指定的桶不存在就创建一个新的,直接new 出一个 Node 来完成
tab[i] = newNode(hash, key, value, null);
} else {
// 指定的桶已经存在
Node<K,V> e; K k;
if (p.hash == hash // 哈希码相同
&& ((k = p.key) == key || (key != null && key.equals(k))) // 键的值相同
) {
// 第一个结点与我们要插入的键值对的键相等
e = p;
} else if (p instanceof TreeNode) {
// 桶的数据结构是红黑树,调用红黑树的方法继续插入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
} else {
// 桶的数据结构是链表,使用链表的处理方式继续插入
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 已经遍历到了链表的结尾,还没有找到,需要新建一个结点
p.next = newNode(hash, key, value, null);
// 插入新结点之后,如果某个链表的长度 >= 8,则要把链表转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, hash);
}
break;
}
if (e.hash == hash // 哈希码相同
&& ((k = e.key) == key || (key != null && key.equals(k))) // 键的值相同
) {
// 说明要插入的键值对的键是存在的,需要更新之前的结点的数据
break;
}
p = e;
}
}
if (e != null) {
// 说明指定的键是存在的,需要更新结点的值
V oldValue = e.value;
if