主要内容:Kotlin, Java, RxJava, 多线程/并发, 集合java
LruCache 用来实现基于内存的缓存,LRU 就是最近最少使用的意思,LruCache 基于 LinkedHashMap 实现。LinkedHashMap 是在 HashMap 的基础之上进行了封装,除了具备哈希功能,还将数据插入到双向链表中维护。每次读取的数据会被移动到链表的尾部,当达到了缓存的最大的容量的时候就将链表的首部移出。使用 LruCache 的时候须要注意的是单位的问题,由于该 API 并不清楚要存储的数据是如何计算大小的,因此它提供了方法供咱们实现大小的计算方式。(《Android 内存缓存框架 LruCache 的源码分析》)git
DiskLruCache 与 LruCache 相似,也是用来实现缓存的,而且也是基于 LinkedHashMap 实现的。不一样的是,它是基于磁盘缓存的,LruCache 是基于内存缓存的。因此,LinkedHashMap 可以存储的空间更大,可是读写的速率也更慢。使用 DiskLruCache 的时候须要到 Github 上面去下载。OkHttp 和 Glide 的磁盘缓存都是基于 DiskLruCache 开发的。DiskLruCahce 内部维护了一个日志文件,记录了读写的记录的信息。其余的基本都是基础的磁盘 IO 操做。github
若是想线程安全地使用这列表类(能够参考下面的问题)面试
咱们有几种方式能够线程间安全地操做 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 (!onlyIfAbsent || oldValue == null) {
e.value = value;
}
return oldValue;
}
}
++modCount;
// 若是插入了新的结点以后,哈希表的容量大于 threshold 就进行扩容
if (++size > threshold) {
resize(); // 扩容
}
return null;
}
复制代码
上面是 HashMap 的插入的逻辑,能够看出,它也是根据头结点的类型,分红红黑树和链表两种方式来进行处理的。对于链表,上面已经给出了具体的插入逻辑。在链表的情形中,除了基础的插入,当链表的长度达到了 8 的时候还要将桶的数据结构从链表转型成为红黑树。对于红黑树类型的数据结构,它调用 TreeNode 的 putTreeVal()
方法来完成往红黑树中插入结点的逻辑。(代码贴出来,慢慢领会吧:))
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
// 查找根节点, 索引位置的头节点并不必定为红黑树的根结点
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) { // 将根节点赋值给 p, 开始遍历
int dir, ph; K pk;
if ((ph = p.hash) > h)
// 若是传入的 hash 值小于 p 节点的 hash 值,则将 dir 赋值为 -1, 表明向 p 的左边查找树
dir = -1;
else if (ph < h)
// 若是传入的 hash 值大于 p 节点的 hash 值,则将 dir 赋值为 1, 表明向 p 的右边查找树
dir = 1;
// 若是传入的 hash 值和 key 值等于 p 节点的 hash 值和 key 值, 则 p 节点即为目标节点, 返回 p 节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 若是 k 所属的类没有实现 Comparable 接口 或者 k 和 p 节点的 key 相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
// 第一次符合条件, 该方法只有第一次才执行
TreeNode<K,V> q, ch;
searched = true;
// 从 p 节点的左节点和右节点分别调用 find 方法进行查找, 若是查找到目标节点则返回
if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null)
|| ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null))
return q;
}
// 使用定义的一套规则来比较 k 和 p 节点的 key 的大小, 用来决定向左仍是向右查找
dir = tieBreakOrder(k, pk); // dir<0 则表明 k<pk,则向 p 左边查找;反之亦然
}
TreeNode<K,V> xp = p; // xp 赋值为 x 的父节点,中间变量,用于下面给x的父节点赋值
// dir<=0 则向 p 左边查找,不然向 p 右边查找,若是为 null,则表明该位置即为 x 的目标位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 走进来表明已经找到 x 的位置,只需将 x 放到该位置便可
Node<K,V> xpn = xp.next; // xp 的 next 节点
// 建立新的节点, 其中 x 的 next 节点为 xpn, 即将 x 节点插入 xp 与 xpn 之间
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0) // 若是时 dir <= 0, 则表明 x 节点为 xp 的左节点
xp.left = x;
else // 若是时 dir> 0, 则表明 x 节点为 xp 的右节点
xp.right = x;
xp.next = x; // 将 xp 的n ext 节点设置为 x
x.parent = x.prev = xp; // 将 x 的 parent 和 prev 节点设置为xp
// 若是 xpn 不为空,则将 xpn 的 prev 节点设置为 x 节点,与上文的 x 节点的 next 节点对应
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x)); // 进行红黑树的插入平衡调整
return null;
}
}
}
复制代码
HashSet 内部经过 HashMap 实现,HashMap 解决哈希冲突使用的是拉链法,碰撞的元素会放进链表中,链表长度超过 8,而且已经使用的桶的数量大于 64 的时候,会将桶的数据结构从链表转换成红黑树。HashMap 在求得每一个结点在数组中的索引的时候,会使用对象的哈希码的高八位和低八位求异或,来增长哈希码的随机性。
HashSet 内部使用 HashMap 实现,当咱们经过 put()
方法将一个键值对添加到哈希表当中的时候,会根据哈希值和键是否相等两个条件进行判断,只有当二者彻底相等的时候才认为元素发生了重复。
HashSet 不容许列表中存在重复的元素,HashSet 内部使用的是 HashMap 实现的。在咱们向 HashSet 中添加一个元素的时候,会将该元素做为键,一个默认的对象做为值,构成一个键值对插入到内部的 HashMap 中。(HashMap 的实现见上文。)
TreeMap 是基于红黑树实现的(后续完善)
Java 注解在 Android 中比较常见的使用方式有 3 种:
bind()
的时候直接反射调用生成的方法。Room 也是在编译期间为使用注解的方法生成数据库方法的。在开发这种第三方库的时候还可能使用到 Javapoet 来帮助咱们生成 Java 文件。@IntDef({/*各类枚举值*/})
来指定整型的取值范围。而后使用注解修饰咱们要方法的参数便可。这样 IDE 会给出一个提示信息,提示咱们只能使用指定范围的值。(《Java 注解及其在 Android 中的应用》)关联:ButterKnife, ARouter
这两个方法都具备决定一个对象身份功能,因此二者的行为必须一致,覆写这两个方法须要遵循必定的原则。能够从业务的角度考虑使用对象的惟一特征,好比 ID 等,或者使用它的所有字段来进行计算获得一个整数的哈希值。通常,我不会直接覆写该方法,除非业务特征很是明显。由于一旦修改以后,它的做用范围将是全局的。咱们还能够经过 IDEA 的 generate 直接生成该方法。
wait() & notify()
, 用来对线程进行控制,以让当前线程等待,直到其余线程调用了 notify()/notifyAll()
方法。wait()
发生等待的前提是当前线程获取了对象的锁(监视器)。调用该方法以后当前线程会释放获取到的锁,而后让出 CPU,进入等待状态。notify/notifyAll()
的执行只是唤醒沉睡的线程,而不会当即释放锁,锁的释放要看代码块的具体执行状况。clone()
与对象克隆相关的方法(深拷贝&浅拷贝?)finilize()
toString()
equal() & hashCode()
,见上前者是线程安全的,每一个方法上面都使用 synchronized 关键字进行了加锁,后者是非线程安全的。通常状况下使用 StringBuilder 便可,由于非多线程环境进行加锁是一种没有必要的开销。
intern()
方法将该字符串对象存储在字符串池,若是字符串池已经有了一样值的字符串,则返回引用。使用双引号直接建立字符串的时候,JVM 先去字符串池找有没有值相等字符串,若是有,则返回找到的字符串引用;不然建立一个新的字符串对象并存储在字符串池。UTF-8 编码把一个 Unicode 字符根据不一样的数字大小编码成 1-6 个字节,经常使用的英文字母被编码成 1 个字节,汉字一般是 3 个字节,只有很生僻的字符才会被编码成 4-6 个字节。
参考文章,了解字符串编码的渊源:字符编码 ASCII UNICODE UTF-8
如何开启线程,线程池参数;注意的问题:线程数量,内存泄漏
// 方式 1:Thread 覆写 run() 方法;
private class MyThread extends Thread {
@Override
public void run() {
// 业务逻辑
}
}
// 方式 2:Thread + Runnable
new Thread(new Runnable() {
public void run() {
// 业务逻辑
}
}).start();
// 方式 3:ExectorService + Callable
ExecutorService executor = Executors.newFixedThreadPool(5);
List<Future<Integer>> results = new ArrayList<Future<Integer>>();
for (int i=0; i<5; i++) {
results.add(executor.submit(new CallableTask(i, i)));
}
复制代码
线程数量的问题:
Android 中并无明确规定能够建立的线程的数量,可是每一个进程的资源是有限的,线程自己会占有必定的资源,因此受内存大小的限制,会有数量的上限。一般,咱们在使用线程或者线程池的时候,不会建立太多的线程。线程池的大小经验值应该这样设置:(其中 N 为 CPU 的核数)
N + 1
;(大部分时间在计算)2N + 1
;(大部分时间在读写,Android)下面是 Android 中的 AysncTask 中建立线程池的代码(建立线程池的核心参数的说明已经家在了注释中),
// CPU 的数量
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// 核心线程的数量:只有提交任务的时候才会建立线程,当当前线程数量小于核心线程数量,新添加任务的时候,会建立新线程来执行任务
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
// 线程池容许建立的最大线程数量:当任务队列满了,而且当前线程数量小于最大线程数量,则会建立新线程来执行任务
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
// 非核心线程的闲置的超市时间:超过这个时间,线程将被回收,若是任务多且执行时间短,应设置一个较大的值
private static final int KEEP_ALIVE_SECONDS = 30;
// 线程工厂:自定义建立线程的策略,好比定义一个名字
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
// 任务队列:若是当前线程的数量大于核心线程数量,就将任务添加到这个队列中
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
public static final Executor THREAD_POOL_EXECUTOR;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
/*corePoolSize=*/ CORE_POOL_SIZE,
/*maximumPoolSize=*/ MAXIMUM_POOL_SIZE,
/*keepAliveTime=*/ KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
/*workQueue=*/ sPoolWorkQueue,
/*threadFactory=*/ sThreadFactory
/*handler*/ defaultHandler); // 饱和策略:AysncTask 没有这个参数
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
复制代码
饱和策略:任务队列和线程池都满了的时候执行的逻辑,Java 提供了 4 种实现;
其余:
prestartAllcoreThread()
方法的时候,线程池会提早启动并建立全部核心线程来等待任务;allowCoreThreadTimeOut()
方法的时候,超时时间到了以后,闲置的核心线程也会被移除。run()
和 start()
方法区别:start()
会调用 native 的 start()
方法,而后 run()
方法会被回调,此时 run()
异步执行;若是直接调用 run()
,它会使用默认的实现(除非覆写了),而且会在当前线程中执行,此时 Thread 如同一个普通的类。
private Runnable target;
public void run() {
if (target != null) target.run();
}
复制代码
线程关闭,有两种方式能够选择,一种是使用中断标志位进行判断。当须要中止线程的时候,调用线程的 interupt()
方法便可。这种状况下须要注意的地方是,当线程处于阻塞状态的时候调用了中断方法,此时会抛出一个异常,并将中断标志位复位。此时,咱们是没法退出线程的。因此,咱们须要同时考虑通常状况和线程处于阻塞时中断两种状况。
另外一个方案是使用一个 volatile 类型的布尔变量,使用该变量来判断是否应该结束线程。
// 方式 1:使用中断标志位
@Override
public void run() {
try {
while (!isInterrupted()) {
// do something
}
} catch (InterruptedException ie) {
// 线程由于阻塞时被中断而结束了循环
}
}
private static class MyRunnable2 implements Runnable {
// 注意使用 volatile 修饰
private volatile boolean canceled = false;
@Override
public void run() {
while (!canceled) {
// do something
}
}
public void cancel() {
canceled = true;
}
}
复制代码
防止线程内存泄漏:
onDestroy()
方法中终止线程。wait()/notify():
wait()、notify() 和 notifyAll()
方法是 Object 的本地 final 方法,没法被重写。wait()
使当前线程阻塞,直到接到通知或被中断为止。前提是必须先得到锁,通常配合 synchronized 关键字使用,在 synchronized 同步代码块里使用 wait()、notify() 和 notifyAll()
方法。若是调用 wait()
或者 notify()
方法时,线程并未获取到锁的话,则会抛出 IllegalMonitorStateException 异常。再次获取到锁,当前线程才能从 wait()
方法处成功返回。wait()、notify() 和 notifyAll()
在 synchronized 代码块执行,说明当前线程必定是获取了锁的。当线程执行 wait()
方法时候,会释放当前的锁,而后让出 CPU,进入等待状态。只有当 notify()/notifyAll()
被执行时候,才会唤醒一个或多个正处于等待状态的线程,而后继续往下执行,直到执行完 synchronized 代码块或是中途遇到 wait()
,再次释放锁。notify()/notifyAll()
的执行只是唤醒沉睡的线程,而不会当即释放锁,锁的释放要看代码块的具体执行状况。因此在编程中,尽可能在使用了 notify()/notifyAll()
后当即退出临界区,以唤醒其余线程。wait()
须要被 try catch
包围,中断也可使 wait
等待的线程唤醒。notify()
和 wait()
的顺序不能错,若是 A 线程先执行 notify()
方法,B 线程再执行 wait()
方法,那么 B 线程是没法被唤醒的。notify()
和 notifyAll()
的区别:notify()
方法只唤醒一个等待(对象的)线程并使该线程开始执行。因此若是有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪一个线程取决于操做系统对多线程管理的实现。notifyAll()
会唤醒全部等待 (对象的) 线程,尽管哪个线程将会第一个处理取决于操做系统的实现。若是当前状况下有多个线程须要被唤醒,推荐使用 notifyAll()
方法。好比在生产者-消费者里面的使用,每次都须要唤醒全部的消费者或是生产者,以判断程序是否能够继续往下执行。对于 sleep()
和 wait()
方法之间的区别,总结以下,
sleep()
方法是 Thread 的静态方法,而 wait()
是 Object 实例方法。wait()
方法必需要在同步方法或者同步块中调用,也就是必须已经得到对象锁。而 sleep()
方法没有这个限制能够在任何地方种使用。wait()
方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而 sleep()
方法只是会让出 CPU 并不会释放掉对象锁;sleep()
方法在休眠时间达到后若是再次得到 CPU 时间片就会继续执行,而 wait()
方法必须等待 Object.notift()/Object.notifyAll()
通知后,才会离开等待池,而且再次得到 CPU 时间片才会继续执行。start()
方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 CPU 的使用权 。o.wait()
方法,JVM 会把该线程放入等待队列 (waitting queue) 中。Thread.sleep(long)
或 t.join()
方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()
状态超时、join()
等待线程终止或者超时、或者 I/O 处理完毕时,线程从新转入 RUNNABLE 状态。run()
、main()
方法执行结束,或者因异常退出了 run()
方法,则该线程结束生命周期。死亡的线程不可再次复生。当两个线程彼此占有对方须要的资源,同时彼此又没法释放本身占有的资源的时候就发生了死锁。发生死锁须要知足下面四个条件,
产生死锁须要四个条件,那么,只要这四个条件中至少有一个条件得不到知足,就不可能发生死锁了。因为互斥条件是非共享资源所必须的,不只不能改变,还应加以保证,因此,主要是破坏产生死锁的其余三个条件。
破坏占有且等待的问题:容许进程只得到运行初期须要的资源,便开始运行,在运行过程当中逐步释放掉分配到的已经使用完毕的资源,而后再去请求新的资源。
破坏不可抢占条件:当一个已经持有了一些资源的进程在提出新的资源请求没有获得知足时,它必须释放已经保持的全部资源,待之后须要使用的时候再从新申请。释放已经保持的资源颇有可能会致使进程以前的工做实效等,反复的申请和释放资源会致使进程的执行被无限的推迟,这不只会延长进程的周转周期,还会影响系统的吞吐量。
破坏循环等待条件:能够经过定义资源类型的线性顺序来预防,可将每一个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于 i 的资源。
sychronized 原理(表面的):
Java 虚拟机中的同步 (Synchronization) 基于进入和退出管程 (Monitor) 对象实现,不管是显式同步 (有明确的 monitorenter 和 monitorexit 指令,即同步代码块),仍是隐式同步都是如此。进入 monitorenter 时 monitor 中的计数器 count 加 1,释放当前持有的 monitor,count 自减 1. 反编译代码以后常常看到两个 monitorexit 指令对应一个 monitorenter,这是用来防止程序执行过程当中出现异常的。虚拟机须要保证即便程序容许中途出了异常,锁也同样能够被释放(执行第二个 monitorexit)。
对同步方法,JVM 能够从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当调用方法时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,若是设置了,执行线程将先持有 monitor,而后再执行方法,最后再方法完成 (不管是正常完成仍是非正常完成) 时释放monitor. 在方法执行期间,其余任何线程都没法再得到同一个 monitor. 若是一个同步方法执行期间抛出了异常,而且在方法内部没法处理此异常,那这个同步方法所持有的 monitor 将在异常抛到同步方法以外时自动释放。
sychronized 原理(底层的):
在 Java 对象的对象头中,有一块区域叫作 MarkWord,其中存储了重量级锁 sychronized 的标志位,其指针指向的是 monitor 对象。每一个对象都存在着一个 monitor 与之关联。在 monitor 的数据结构中定义了两个队列,_WaitSet 和 _EntryList. 当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把 monitor 中的 owner 变量设置为当前线程同时 monitor 中的计数器 count 加 1,若线程调用 wait() 方法,将释放当前持有的 monitor,owner 变量恢复为 null,count 自减 1,同时该线程进入 _WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放 monitor (锁)并复位变量的值,以便其余线程进入获取 monitor (锁)。
由此看来,monitor 对象存在于每一个 Java 对象的对象头中(存储的指针的指向),synchronized 锁即是经过这种方式获取锁的,也是为何 Java 中任意对象能够做为锁的缘由,同时也是 notify()/notifyAll()/wait()
等方法存在于顶级对象 Object 中的缘由。
固然,从 MarkWord 的结构中也能够看出 Java 对 sychronized 的优化:Java 6 以后,为了减小得到锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,锁效率也获得了优化。
(关于 sychronized 的底层实现原理能够参考笔者的文章:并发编程专题 3:synchronized)
sychronized 与 lock 的区别体如今下面四个方面:
ReentrantLock 的实现原理:
ReentrantLock 的实现是基于 AQS(同步器),同步器设计的思想是 CAS. 同步器中维护了一个链表,借助 CAS 的思想向链表中增删数据。其底层使用的是 sun.misc.Unsafe
类中的方法来完成 CAS 操做的。在 ReentrantLock 中实现两个 AQS 的子类,分别是 NonfairSync
和 FairSync
. 也就是用来实现公平锁和非公平锁的关键。当咱们使用构造方法获取 ReentrantLock 实例的时候,能够经过一个布尔类型的参数指定使用公平锁仍是非公平锁。在实现上, NonfairSync
和 FairSync
的区别仅仅是,在当前线程获取到锁以前,是否会从上述队列中判断是否存在比本身更早申请锁的线程。对于公平锁,当存在这么一个线程的话,那么当前线程获取锁失败。当当前线程获取到锁的时候,也会使用一个 CAS 操做将锁获取次数 +1. 当线程再次获取锁的时候,会根据线程来进行判断,若是当前持有锁的线程是申请锁的线程,那么容许它再次获取锁,以此来实现锁的可重入。
所谓 CAS 就是 Compare-And-Swape,相似于乐观加锁。但与咱们熟知的乐观锁不一样的是,它在判断的时候会涉及到 3 个值:“新值”、“旧值” 和 “内存中的值”,在实现的时候会使用一个无限循环,每次拿 “旧值” 与 “内存中的值” 进行比较,若是两个值同样就说明 “内存中的值” 没有被其余线程修改过;不然就被修改过,须要从新读取内存中的值为 “旧值”,再拿 “旧值” 与 “内存中的值” 进行判断。直到 “旧值” 与 “内存中的值” 同样,就把 “新值” 更新到内存当中。
这里要注意上面的 CAS 操做是分 3 个步骤的,可是这 3 个步骤必须一次性完成,由于否则的话,当判断 “内存中的值” 与 “旧值” 相等以后,向内存写入 “新值” 之间被其余线程修改就可能会获得错误的结果。JDK 中的 sun.misc.Unsafe
中的 compareAndSwapInt 等一系列方法 Native 就是用来完成这种操做的。另外还要注意,上面的 CAS 操做存在一些问题:
AtomicReference
。voliate 关键字的两个做用
volatile 是经过内存屏障(Memory Barrier)
来实现其在 JMM 中的语义的。内存屏障,又称内存栅栏,是一个 CPU 指令,它的做用有两个,一是保证特定操做的执行顺序,二是保证某些变量的内存可见性。若是在指令间插入一条内存屏障则会告诉编译器和 CPU,无论什么指令都不能和这条 Memory Barrier 指令重排序。Memory Barrier 的另一个做用是强制刷出各类 CPU 的缓存数据,所以任何 CPU 上的线程都能读取到这些数据的最新版本。
参考 《并发编程专题-5:生产者和消费者模式》 中的三种写法。
ThreadLocal 经过将每一个线程本身的局部变量存在本身的内部来实现线程安全。使用它的时候会定义它的静态变量,每一个线程看似是从 TL 中获取数据,而实际上 TL 只起到了键值对的键的做用,实际的数据会以哈希表的形式存储在 Thread 实例的 Map 类型局部变量中。当调用 TL 的 get()
方法的时候会使用 Thread.currentThread()
获取当前 Thread 实例,而后从该实例的 Map 局部变量中,使用 TL 做为键来获取存储的值。Thread 内部的 Map 使用线性数组解决哈希冲突。(《ThreadLocal的使用及其源码实现》)
poll()
和 add()
等方法借助 CAS 思想实现。锁比较轻量。NIO
多线程断点续传原理
断点续传和断点下载都是用的用的都是 RandomAccessFile,它能够从指定的位置开始读取数据。断点续传是由服务器给客户端一个已经上传的位置标记position,而后客户端再将文件指针移动到相应的 position,经过输入流将文件剩余部分读出来传输给服务器。
若是要使用多线程来实现断点续传,那么能够给每一个线程分配固定的字节的文件,分别去读,而后分别上传到服务器。
协程实际上就是极大程度的复用线程,经过让线程满载运行,达到最大程度的利用 CPU,进而提高应用性能。相比于线程,协程不须要进行线程切换,和多线程比,线程数量越多,协程的性能优点就越明显。第二大优点就是不须要多线程的锁机制,由于只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只须要判断状态就行了,因此执行效率比多线程高不少。
协程和线程,都能用来实现异步调用,可是这二者之间是有本质区别的:
编译器
级别的,线程是系统级别的。协程的切换是由程序来控制
的,线程的切换是由操做系统来控制的。协做式
的,线程是抢占式
的。协程是由程序来控制何时进行切换的,而线程是有操做系统来决定线程之间的切换的。一个线程
中执行。4. 协程适合 IO 密集型
的程序,多线程适合 计算密集型
的程序(适用于多核 CPU 的状况)。当你的程序大部分是文件读写操做或者网络请求操做的时候,这时你应该首选协程而不是多线程,首先这些操做大部分不是利用 CPU 进行计算而是等待数据的读写,其次由于协程执行效率较高,子程序切换不是线程切换,是由程序自身控制,所以,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优点就越明显。顺序调用
异步代码,避免回调地狱
。参考:是继续Rxjava,仍是应该试试Kotlin的协程 - Android架构的文章 - 知乎
Kotlin 是一门基于 JVM 的语言,它提供了很是多便利的语法特性。若是说 Kotlin 为何那么优秀的话,那只能说是由于它站在了 Java 的肩膀上。学习了一段时间以后,你会发现它的许多语法的设计很是符合咱们实际开发中的使用习惯。
好比,对于一个类,一般咱们不会去覆写它。尤为是 Java Web 方向,不少的类用来做为 Java Bean,它们没有特别多的继承关系。而 Kotlin 中的类默认就是不容许继承的,想容许本身的类被继承,你还必须显式地使用 open 关键字指定。
对于 Java Bean,做为一个业务对象,它会有许多的字段。按照 Java 中的处理方式,咱们要为它们声明一系列的 setter 和 getter 方法。而后,获取属性的时候必须使用 setter 和 getter 方法。致使咱们的代码中出现很是多的括号。而使用 Kotlin 则能够直接对属性进行赋值,显得优雅地多。
再好比 Java 中使用 switch 的时候,咱们一般会在每一个 case 后面加上 break,而 kotlin 默认帮助咱们 break,这样就节省了不少的代码量。
另外 Kotlin 很是优秀的地方在于对 NPE 的控制。在 Android 开发中,咱们可使用 @NoneNull 和 @Nullable 注解来标明某个字段是否可能为空。在 Java 中默认字段是空的,而且没有任何提示。你一个不留神可能就致使了 NPE,但 Kotlin 中就默认变量是非空的,你想让它为空必须单独声明。这样,对于可能为空的变量就给了咱们提示的做用,咱们知道它可能为空,就会去特地对其进行处理。对于可能为空的类,Kotlin 定义了以下的规则,使得咱们处理起来 NPE 也变得很是简单:
?
在类型的后面则说明这个变量是可空的;?.
,以 a?.method()
为例,当 a 不为 null 则整个表达式的结果是 a.method()
不然是 null;?:
,以 a ?: "A"
为例,当 a 不为 null 则整个表达式的结果是 a,不然是 “A”;as?
,以 foo as? Typ
e 为例,当 foo 是 Type 类型则将 foo 转换成 Type 类型的实例,不然返回 null;!!
,用在某个变量后面表示断言其非空,如 a!!
;val b = "AA".let { it + "A" }
返回 “AAA”;诸如此类,不少时候,我以为 Java 设计的一些规则对人们产生了误导,实际开发中并不符合咱们的使用习惯。而 Kotlin 则是根据多年来人们使用 Java 的经验,简化了许多的调用,更加符合咱们使用习惯。因此说,Kotlin 之因此强大是由于站在 Java 的肩膀上。
观察者设计模式相似于咱们常用的接口回调,下面的代码中在观察者的构造方法中订阅了主题,其实这个倒不怎么重要,何时订阅均可以。核心的地方就是主题中维护的这个队列,须要通知的时候调一下通知的方法便可。另外,若是在多线程环境中还要考虑如何进行线程安全控制,好比使用线程安全的集合等等。下面只是一个很是基础的示例程序,了解设计思想,用的时候能够灵活一些,没必要循规蹈矩。
public class ConcreteSubject implements Subject {
private List<Observer> observers = new LinkedList<>(); // 维护观察者列表
@Override
public void registerObserver(Observer o) { // 注册一个观察者
observers.add(o);
}
@Override
public void removeObserver(Observer o) { // 移除一个观察者
int i = observers.indexOf(o);
if (i >= 0) {
observers.remove(o);
}
}
@Override
public void notifyObservers() { // 通知全部观察者主题的更新
for (Observer o : observers) {
o.method();
}
}
}
public class ConcreteObserver implements Observer {
private Subject subject; // 该观察者订阅的主题
public ConcreteObserver(Subject subject) {
this.subject = subject;
subject.registerObserver(this); // 将当前观察者添加到主题订阅列表中
}
// 当主题发生变化的时候,主题会遍历观察者列表并经过调用该方法来通知观察者
@Override
public void method() {
// ...
}
}
复制代码
(了解更多关于观察者设计模式的内容,请参考文章:设计模式解析:观察者模式)
// 饱汉:就是在调用单例方法的时候,实例已经初始化过了
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return singleton;
}
}
// 懒汉:在调用方法的时候才进行初始化
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
sychronized(Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
复制代码
另外,单例须要注意的问题是:1.若是用户使用反射进行初始化怎么办?能够在建立第二个实例的时候抛出异常;2.若是用户使用 Java 的序列化机制反复建立单例呢?将全部的实例域设置成 transient 的,而后覆写 readResolve()
方法并返回单例。
另外,单实例太多的时候能够想办法使用一个 Map 将它们存储起来,而后经过一种规则从哈希表中取出,这样就不必声明一大堆的单例变量了。
(了解更多关于单例设计模式的内容,请参考文章:设计模式-4:单例模式)
四个设计模式相同的地方是,它们都须要你传入一个类,而后内部使用你传入的这个类来完成业务逻辑。
咱们以字母 A,B,C 来表示 3 种不一样的类(某种东西)。
外观模式要隐藏内部的差别,提供一个一致的对外的接口 X,那么让定义 3 个类 AX, BX, CX 而且都实现 X 接口,其中分别引用 A, B, C 按照各自的方式实现 X 接口的方法便可。以相机开发为例,Camera1 和 Camera2 各有本身的实现方式,定义一个统一的接口和两个实现类。
假如如今有一个类 X,其中引用到了接口 A 的实现 AX. AX 的逻辑存在点问题,咱们想把它完善一下。咱们提供了 3 种方案,分别是 A1, A2 和 A3. 那么此时,咱们让 A1, A2 和 A3 都实现 A 接口,而后其中引用 AX 完成业务,在实现的 A 接口的方法中分别使用各自的方案进行优化便可。这种方式,咱们对 AX 进行了修饰,使其 A1, A2 和 A3 能够直接应用到 X 中。
对于适配器模式,假如如今有一个类 X,其中引用到了接口 A. 如今咱们不得不使用 B 来完成 A 的逻辑。由于 A 和 B 属于两个不一样的类,因此此时咱们须要一个适配器模式,让 A 的实现 AX 引用 B 的实现 BX 完成 A 接口的各个方法。
外观模式的目的是隐藏各种间的差别性,提供一致的对外接口。装饰者模式对外的接口是一致的,可是内部引用的实例是同一个,其目的是对该实例进行拓展,使其具备多种功能。因此,前者是多对一,后者是一对多的关系。而适配器模式适用的是两个不一样的类,它使用一种类来实现另外一个类的功能,是一对一的。相比之下,代理模式也是用一类来完成某种功能,而且一对一,但它是在同类之间,目的是为了加强类的功能,而适配器是在不一样的类之间。装饰者和代理都用来加强类的功能,可是装饰者装饰以后仍然是同类,能够无缝替换以前的类的功能。而代理类被修饰以后已是代理类了,是另外一个类,没法替换原始类的位置。
Android 高级面试系列文章,关注做者及时获取更多面试资料,
本系列以及其余系列的文章均维护在 Github 上面:Github / Android-notes,欢迎 Star & Fork. 若是你喜欢这篇文章,愿意支持做者的工做,请为这篇文章点个赞👍!