大厂都爱问到多线程?27道关于多线程的总结助你一臂之力

本文主要是整理了中高级安卓须要会多线程模块的(或者说面试被频繁问到的内容),主要做为参考大纲,以后会陆续更新每一个详细部分,供你们参考,互相学习。java

面试板块(PDF版以下):

BAT面试合集(Binder,组件化插件化,热修复,AOP,QQ换肤,虚拟机,https,线程池原理,音视频原理)
算法合集(Hash,KMP 等)
中小厂面试合集(内存泄漏,Handler,View,MVC.MVP.MVVM,)
大厂相关更新技术(Glide,数据库,NDK)
面试小知识(java小知识)
设计模式(设计模式原则和分类)
数据结构(数据结构等等)
网络编程(三次握手和四次握手,Volley,OKHttps,Retrofit)
源码解析(属性动画实现原理等)
多线程解析(线程同步,进程线程)
性能优化(Webview,内存泄漏和内存溢出等)android

大厂都爱问到多线程?27道关于多线程的总结助你一臂之力
顺手留下GitHub连接,须要获取相关面试或者面试宝典核心笔记PDF等内容的能够本身去找
https://github.com/xiangjiana/Android-MS
(VX:mm14525201314)git

一丶 什么是线程

线程就是进程中运行的多个子任务,是操做系统调用的最小单元程序员

二丶线程的状态

New: 新建状态,new 出来,尚未调用 start
Runnable: 可运行状态,调用 start 进入可运行状态,可能运行也可能没有运行,取决于操做系统的调度
Blocked: 阻塞状态,被锁阻塞,暂时不活动,阻塞状态是线程阻塞在进入synchronized 关键字修饰的方法或代码块(获取锁)时的状态。
Waiting: 等待状态,不活动,不运行任何代码,等待线程调度器调度,wait sleep
Timed Waiting: 超时等待,在指定时间自行返回
Terminated: 终止状态,包括正常终止和异常终止github

三丶线程的建立

a.继承 Thread 重写 run 方法
b.实现 Runnable 重写 run 方法
c.实现 Callable 重写 call 方法
实现 Callable 和实现 Runnable 相似,可是功能更强大,具体表如今
a.能够在任务结束后提供一个返回值,Runnable 不行
b.call 方法能够抛出异常,Runnable 的 run 方法不行
c.能够经过运行 Callable 获得的 Fulture 对象监听目标线程调用 call 方法的结果,获得返回值,(fulture.get(),调用后会阻塞,直到获取到返回值)面试

四丶线程中断

通常状况下,线程不执行完任务不会退出,可是在有些场景下,咱们须要手动控制线程中断结束任务,Java 中有提供线程中断机制相关的 Api,每一个线程都一个状态位用于标识当前线程对象是不是中断状态算法

public boolean isInterrupted() //判断中断标识位是不是 true,不会改变标
识位 public void interrupt() //将中断标识位设置为 truepublic static<br/>boolean interrupted() //判断当前线程是否被中断,而且该方法调用结束的时候会清空中断标识位须要注意的是 interrupt()方法并不会真的中断线程,它只是将中断标识位设置为 true,具体是否要中断由程序来判断,以下,只要线程中断标识位为 false,也就是没有中断就一直执行线程方法数据库

new Thread( new Runnable(){
          while (!Thread.currentThread().isInterrupted()){
                  //执行线程方法
          }
  }).start();

前边咱们提到了线程的六种状态,New Runnable Blocked Waiting Timed WaitingTerminated,那么在这六种状态下调用线程中断的代码会怎样呢,New 和Terminated 状态下,线程不会理会线程中断的请求,既不会设置标记位,在Runnable 和 Blocked 状态下调用 interrupt 会将标志位设置位 true,在 Waiting 和Timed Waiting 状态下会发生 InterruptedException 异常,针对这个异常咱们如何处理?编程

1.在 catch 语句中经过 interrupt 设置中断状态,由于发生中断异常时,中断标志位会被复位,咱们须要从新将中断标志位设置为 true,这样外界能够经过这个状态判断是否须要中断线程设计模式

try {
  ....
  } catch (InterruptedException e){
    Thread.currentThread().interrupt();
  }

2.更好的作法是,不捕获异常,直接抛出给调用者处理,这样更灵活

五丶Thread为何不能用stop方法中止线程

从 SUN 的官方文档能够得知,调用 Thread.stop()方法是不安全的,这是由于当调用 Thread.stop()方法时,会发生下面两件事:

  1. 即刻抛出 ThreadDeath 异常,在线程的 run()方法内,任何一点都有可能抛出ThreadDeath Error,包括在 catch 或 finally 语句中。
  2. 释放该线程所持有的全部的锁。调用 thread.stop()后致使了该线程所持有的全部锁的忽然释放,那么被保护数据就有可能呈现不一致性,其余线程在使用这些被破坏的数据时,有可能致使一些很奇怪的应用程序错误。

六丶 重入锁与条件对象,同步方法和同步代码块

。。。

七丶volatile 关键字

volatile 为实例域的同步访问提供了免锁机制,若是声明一个域为volatile,那么编译器和虚拟机就直到该域可能被另外一个线程并发更新

八丶 java内存模型

堆内存是被全部线程共享的运行时内存区域,存在可见性的问题。线程之间共享变量存储在主存中,每一个线程都有一个私有的本地内存,本地内存存储了该线程共享变量的副本(本地内存是一个抽象概念,并不真实存在),两个线程要通讯的话,首先 A 线程把本地内存更新过的共享变量更新到主存中,而后 B 线程去主存中读取 A 线程更新过的共享变量,也就是说假设线程 A 执行了 i = 1 这行代码更新主线程变量 i 的值,会首先在本身的工做线程中堆变量 i 进行赋值,而后再写入主存当中,而不是直接写入主存

九丶原子性 性 可见性 有序性

原子性: 对基本数据类型的读取和赋值操做是原子性操做,这些操做不可被中断,是一步到位的,例如 x=3 是原子性操做,而 y = x 就不是,
它包含两步: 第一读取 x,第二将 x 写入工做内存;x++也不是原子性操做,它包含三部,第一,读取x,第二,对 x 加 1,第三,写入内存。
原子性操做的类如: AtomicInteger AtomicBoolean AtomicLong AtomicReference

可见性: 指线程之间的可见性,既一个线程修改的状态对另外一个线程是可见的。volatile 修饰能够保证可见性,它会保证修改的值会当即被更新到主存,因此对其余线程是可见的,普通的共享变量不能保证可见性,由于被修改后不会当即写入主存,什么时候被写入主存是不肯定的,因此其余线程去读取的时候可能读到的仍是旧值

有序性: Java 中的指令重排序(包括编译器重排序和运行期重排序)能够起到优化代码的做用,可是在多线程中会影响到并发执行的正确性,使用 volatile 能够保证有序性,禁止指令重排

volatile 能够保证可见性 有序性,可是没法保证原子性,在某些状况下能够提供优于锁的性能和伸缩性,替代 sychronized 关键字简化代码,可是要严格遵循使用条件。

10. 线程池 ThreadPoolExecutor

线程池的工做原理: 线程池能够减小建立和销毁线程的次数,从而减小系统资源的消耗,当一个任务提交到线程池时

a. 首先判断核心线程池中的线程是否已经满了,若是没满,则建立一个核心线程执行任务,不然进入下一步
b. 判断工做队列是否已满,没有满则加入工做队列,不然执行下一步
c. 判断线程数是否达到了最大值,若是不是,则建立非核心线程执行任务,不然执行饱和策略,默认抛出异常

十一丶 线程池的种类

1.FixedThreadPool: 可重用固定线程数的线程池,只有核心线程,没有非核心线程,核心线程不会被回收,有任务时,有空闲的核心线程就用核心线程执行,没有则加入队列排队
2.SingleThreadExecutor: 单线程线程池,只有一个核心线程,没有非核心线程,当任务到达时,若是没有运行线程,则建立一个线程执行,若是正在运行则加入队列等待,能够保证全部任务在一个线程中按照顺序执行,和 FixedThreadPool 的区别只有数量
3.CachedThreadPool: 按需建立的线程池,没有核心线程,非核心线程有Integer.MAX_VALUE 个,每次提交任务若是有空闲线程则由空闲线程执行,没有空闲线程则建立新的线程执行,适用于大量的须要当即处理的而且耗时较短的任务
4.ScheduledThreadPoolExecutor: 继承自 ThreadPoolExecutor,用于延时执行任务或按期执行任务,核心线程数固定,线程总数为Integer.MAX_VALUE

十二丶线程同步机制与原理

为何须要线程同步?当多个线程操做同一个变量的时候,存在这个变量什么时候对另外一个线程可见的问题,也就是可见性。每个线程都持有主存中变量的一个副本,当他更新这个变量时,首先更新的是本身线程中副本的变量值,而后会将这个值更新到主存中,可是是否当即更新以及更新到主存的时机是不肯定的这就致使当另外一个线程操做这个变量的时候,他从主存中读取的这个变量仍是旧的值,致使两个线程不一样步的问题。

线程同步就是为了保证多线程操做的可见性和原子性,好比咱们用synchronized 关键字包裹一端代码,咱们但愿这段码执行完成后,对另外一个线程当即可见,另外一个线程再次操做的时候获得的是上一个线程更新以后的内容,还有就是保证这段代码的原子性,这段代码可能涉及到了好几部操做,咱们但愿这好几步的操做一次完成不会被中间打断,锁的同步机制就能够实现这一点。通常说的 synchronized 用来作多线程同步功能,其实synchronized 只是提供多线程互斥,而对象的 wait()和 notify()方法才提供线程的同步功能。

JVM 经过 Monitor 对象实现线程同步,当多个线程同时请求synchronized 方法或块时,monitor 会设置几个虚拟逻辑数据结构来管理这些多线程。新请求的线程会首先被加入到线程排队队列中,线程阻塞,当某个拥有锁的线程 unlock 以后,则排队队列里的线程竞争上岗(synchronized 是不公平竞争锁,下面还会讲到)。若是运行的线程调用对象的 wait()后就释放锁并进入 wait线程集合那边,当调用对象的 notify()notifyall()后,wait 线程就到排队那边。这是大体的逻辑。

十三丶arrayList 与 linkedList 的读写时间复杂度

(1)ArrayList ArrayList 是一个泛型类,底层采用数组结构保存对象。数组结构的优势是便于对集合进行快速的随机访问,即若是须要常常根据索引位置访问集合中的对象,使用由 ArrayList 类实现的 List 集合的效率较好。数组结构的缺点是向指定索引位置插入对象和删除指定索引位置对象的速度较慢,而且插入或删除对象的索引位置越小效率越低,缘由是当向指定的索引位置插入对象时,会同时将指定索引位置及以后的全部对象相应的向后移动一位。

(2)LinkedList LinkedList 是一个泛型类,底层是一个双向链表,因此它在执行插入和删除操做时比 ArrayList 更加的高效,但也由于链表的数据结构,因此在随机访问方面要比 ArrayList 差。

ArrayList 是线性表(数组)
get()直接读取第几个下标,复杂度 O(1)
add(E)添加元素,直接在后面添加,复杂度 O(1)
add(index, E)添加元素,在第几个元素后面插入,后面的元素须要向后移动,复杂度 O(n)
remove()删除元素,后面的元素须要逐个移动,复杂度 O(n)LinkedList是链表的操做
get()获取第几个元素,依次遍历,复杂度 O(n)
add(E) 添加到末尾,复杂度 O(1)
add(index, E) 添加第几个元素后,须要先查找到第几个元素,直接指针指向操做,复杂度 O(n)
remove()删除元素,直接指针指向操做,复杂度 O(1)

十四丶 为何HashMap 线程不安全(hash碰撞与扩容致使)

HashMap 的底层存储结构是一个 Entry 数组,每一个 Entry 又是一个单链表,一旦发生 Hash 冲突的的时候,HashMap 采用拉链法解决碰撞冲突,由于 hashMap 的put 方法不是同步的,因此他的扩容方法也不是同步的,在扩容过程当中,会新生成一个新的容量的数组,而后对原数组的全部键值对从新进行计算和写入新的数组,以后指向新生成的数组。当多个线程同时检测到 hashmap 须要扩容的时候就会同时调用 resize 操做,各自生成新的数组并 rehash 后赋给该 map 底层的数组 table,结果最终只有最后一个线程生成的新数组被赋给 table 变量,其余线程的均会丢失。并且当某些线程已经完成赋值而其余线程刚开始的时候,就会用已经被赋值的 table 做为原始数组,这样也会有问题。扩容的时候 可能会引起链表造成环状结构

十五丶 进程线程的区别

1.地址空间: 同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
2.资源拥有: 同一进程内的线程共享本进程的资源如内存、I/O、cpu 等,可是进程之间的资源是独立的。
3.一个进程崩溃后,在保护模式下不会对其余进程产生影响,可是一个线程崩溃整个进程都死掉。因此多进程要比多线程健壮
4.进程切换时,消耗的资源大,效率不高。因此涉及到频繁的切换时,使用线程要好于进程。一样若是要求同时进行而且又要共享某些变量的并发操做,只能用线程不能用进程
5.执行过程: 每一个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。可是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
6.线程是处理器调度的基本单位,可是进程不是。
7.二者都可并发执行。

十六丶Binder 的内存拷贝过程

相比其余的 IPC 通讯,好比消息机制、共享内存、管道、信号量等,Binder 仅需一次内存拷贝,便可让目标进程读取到更新数据,同共享内存同样至关高效,其余的 IPC 通讯机制大多须要 2 次内存拷贝。

Binder 内存拷贝的原理为: 进程 A 为Binder 客户端,在 IPC 调用前,需将其用户空间的数据拷贝到 Binder 驱动的内核空间,因为进程 B 在打开 Binder 设备(/dev/binder)时,已将 Binder 驱动的内核空间映射(mmap)到本身的进程空间,因此进程 B 能够直接看到 Binder 驱动内核空间的内容改动

十七丶传统 IPC 机制的通讯原理(2次内存拷贝)

1.发送方进程经过系统调用(copy_from_user)将要发送的数据存拷贝到内核缓存区中。
2.接收方开辟一段内存空间,内核经过系统调用(copy_to_user)将内核缓存区中的数据拷贝到接收方的内存缓存区。
几种传统 IPC 机制存在 2 个问题:

1.须要进行 2 次数据拷贝,第 1 次是从发送方用户空间拷贝到内核缓存区,第 2次是从内核缓存区拷贝到接收方用户空间。
2.接收方进程不知道事先要分配多大的空间来接收数据,可能存在空间上的浪费。

十八丶Java内存模型 (记住堆栈是内存分区,不是模型)

Java 内存模型(即 Java Memory Model,简称 JMM)自己是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,经过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。因为 JVM 运行程序的实体是线程,而每一个线程建立时 JVM 都会为其建立一个工做内存(有些地方称为栈空间),用于存储线程私有的数据,而 Java 内存模型中规定全部变量都存储在主内存,主内存是共享内存区域,全部线程均可以访问,但线程对变量的操做(读取赋值等)必须在工做内存中进行,首先要将变量从主内存拷贝的本身的工做内存空间,而后对变量进行操做,操做完成后再将变量写回主内存,不能直接操做主内存中的变量,工做内存中存储着主内存中的变量副本拷贝,前面说过,工做内存是每一个线程的私有数据区域,所以不一样的线程间没法访问对方的工做内存,线程间的通讯(传值)必须经过主内存来完成

十九丶类的加载过程

类加载过程主要包含加载、验证、准备、解析、初始化、使用、卸载七个方面,下面一一阐述。
1.加载: 获取定义此类的二进制字节流,生成这个类的 java.lang.Class 对象
2.验证: 保证 Class 文件的字节流包含的信息符合 JVM 规范,不会给 JVM 形成危害
3.准备: 准备阶段为变量分配内存并设置类变量的初始化
4.解析: 解析过程是将常量池内的符号引用替换成直接引用
5.初始化: 不一样于准备阶段,本次初始化,是根据程序员经过程序制定的计划去初始化类的变量和其余资源。这些资源有 static{}块,构造函数,父类的初始化等
6.使用:使用过程就是根据程序定义的行为执行
7.卸载: 卸载由 GC 完成

二十丶 什么状况下会触发类的初始化

一、 遇到 newgetstaticputstaticinvokestatic 这 4 条指令;
二、 使用 java.lang.reflect 包的方法对类进行反射调用;
三、 初始化一个类的时候,若是发现其父类没有进行过初始化,则先初始化其父类(注意!若是其父类是接口的话,则不要求初始化父类);
四、 当虚拟机启动时,用户须要指定一个要执行的主类(包含 main 方法的那个类),虚拟机会先初始化这个主类;
五、 当使用 jdk1.7 的动态语言支持时,若是一个java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstaticREF_putstatic,REF_invokeStatic 的方法句柄,而且这个方法句柄所对应的类没有进行过初始化,则先触发其类初始化

二十一丶双亲委托模式

类加载器查找 class 所采用的是双亲委托模式,所谓双亲委托模式就是判断该类是否已经加载,若是没有则不是自身去查找而是委托给父加载器进行查找,这样依次进行递归,直到委托到最顶层的 Bootstrap ClassLoader,若是 Bootstrap ClassLoader 找到了该 Class,就会直接返回,若是没找到,则继续依次向下查找,若是还没找到则最后交给自身去查找

二十二丶双亲委托模式的好处

1. 避免重复加载,若是已经加载过一次 Class,则不须要再次加载,而是直接读取已经加载的 Class
2. 更加安全,确保,java 核心 api 中定义类型不会被随意替换,好比,采用双亲委托模式可使得系统在 Java 虚拟机启动时旧加载了 String 类,也就没法用自定义的 String 类来替换系统的 String 类,这样即可以防止核心 API 库被随意篡改。

二十三丶死锁的产生条件,如何避免死锁

死锁的四个必要条件
1.互斥条件: 一个资源每次只能被一个进程使用
2.请求与保持条件: 进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其余进程占有,此时请求进程被阻塞,但对本身已得到的资源保持不放。
3.不可剥夺条件: 进程所得到的资源在未使用完毕以前,不能被其余进程强行夺走,即只能 由得到该资源的进程本身来释放(只能是主动释放)。
4.循环等待条件: 若干进程间造成首尾相接循环等待资源的关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不知足,就不会发生死锁。

避免死锁的方法: 系统对进程发出每个系统可以知足的资源申请进行动态检查,并根据检查结果决定是否分配资源,若是分配后系统可能发生死锁,则不予分配,不然予以分配,这是一种保证系统不进入死锁状态的动态策略。

在资源的动态分配过程当中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。 通常来讲互斥条件是没法破坏的,因此在预防死锁时主要从其余三个方面入手 :
(1)破坏请求和保持条件: 在系统中不容许进程在已得到某种资源的状况下,申请其余资源,即要想出一个办法,阻止进程在持有资源的同时申请其它资源。
方法一: 在全部进程开始运行以前,必须一次性的申请其在整个运行过程当中所需的所有资源,
方法二: 要求每一个进程提出新的资源申请前,释放它所占有的资源
(2)破坏不可抢占条件: 容许对资源实行抢夺。
方式一: 若是占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,若是有必要,可再次请求这些资源和另外的资源。
方式二: 若是一个进程请求当前被另外一个进程占有的资源,则操做系统能够抢占另外一个进程,要求它释放资源,只有在任意两个进程的优先级都不相同的条件下,该方法才能预防死锁。
(3)破坏循环等待条件
对系统全部资源进行线性排序并赋予不一样的序号,这样咱们即可以规定进程在申请资源时必须按照序号递增的顺序进行资源的申请,当之后要申请时需检查要申请的资源的编号大于当前编号时,才能进行申请。
利用算法避免死锁:
所谓银行家算法,是指在分配资源以前先看清楚,资源分配后是否会致使系统死锁。若是会死锁,则不分配,不然就分配。按照银行家算法的思想,当进程请求资源时,系统将按以下原则分配系统资源

二十四丶App启动流程

App 启动时,AMS 会检查这个应用程序所须要的进程是否存在,不存在就会请求Zygote 进程启动须要的应用程序进程,Zygote 进程接收到 AMS 请求并经过 fock自身建立应用程序进程,这样应用程序进程就会获取虚拟机的实例,还会建立Binder 线程池(ProcessState.startThreadPool())和消息循环(ActivityThread looper.loop),而后 App 进程,经过 Binder IPC 向sytem_server 进程发起attachApplication 请求;system_server 进程在收到请求后,进行一系列准备工做后,再经过 Binder IPC 向 App 进程发送scheduleLaunchActivity 请求;App 进程的binder (ApplicationThread)在收到请求后,经过 handler 向主线程发送LAUNCH_ACTIVITY 消息;主线程在收到 Message 后,经过反射机制建立目标Activity,并回调 Activity.onCreate()等方法。到此,App 便正式启动,开始进入Activity 生命周期,执行完 onCreate/onStart/onResume 方法,UI 渲染结束后即可以看到 App 的主界面。

二十五丶Android 单线程模型

Android 单线程模型的核心原则就是: 只能在 UI 线程(Main Thread)中对 UI 进行处理。当一个程序第一次启动时,Android 会同时启动一个对应的 主线程(Main Thread),主线程主要负责处理与 UI 相关的事件,如:用户的按键事件,用户接触屏幕的事件以及屏幕绘图事 件,并把相关的事件分发到对应的组件进行处理。因此主线程一般又被叫作 UI 线 程。在开发 Android 应用时必须遵照单线程模型的原则: Android UI 操做并非线程安全的而且这些操做必须在 UI 线程中执行。

Android 的单线程模型有两条原则:

1.不要阻塞 UI 线程。
2.不要在 UI 线程以外访问 Android UI toolkit(主要是这两个包中的组件:android.widget and android.view

二十六丶RecyclerView 在不少方面能取代 ListView ,Google 为何没把ListView划上一条过期的横线?

ListView采用的是 RecyclerBin 的回收机制在一些轻量级的 List 显示时效率更高。

二十七丶HashMap 如何保证元素均匀分布

hash & (length-1)
经过 Key 值的 hashCode 值和 hashMap长度-1 作与运算
hashmap中的元素,默认状况下,数组大小为 16,也就是 2 的 4 次方,若是要自定义 HashMap 初始化数组长度,也要设置为 2 的 n 次方大小,由于这样效率最高。由于当数组长度为 2 的 n 次幂的时候,不一样的 key 算出的 index 相同的概率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的概率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了

顺手留下GitHub连接,须要获取相关面试等内容的能够本身去找
https://github.com/xiangjiana/Android-MS

PDF获取
相关文章
相关标签/搜索