Android面试题之多线程,包括线程、Java同步问题、阻塞队列、线程池、AsyncTask、HandlerThread、IntentService等内容。
本文是我一点点归纳总结的干货,但是难免有疏忽和遗漏,希望不吝赐教。
转载请注明链接:https://blog.csdn.net/feather_wch/article/details/81207725
有帮助的话请点个赞!万分感谢!
Android面试题-多线程(99题)
2018/8/13(23:45)
进程
1、什么是进程
- 系统分配资源的最小单位
- 进程就是程序运行的实体
线程
2、什么是线程
- 系统调度的最小单位
- 一个进程中可以包含多个线程
- 线程拥有各自的计数器、堆栈和局部变量等属性,能够访问共享的内存变量
3、线程的好处
- 比进程的资源消耗要小,效率要更高
- 多线程的并发性能减少程序的响应时间
- 多线程能简化程序的结构,使程序便于理解和维护
4、线程的状态有哪些?
状态 | 解释 | 备注 |
---|---|---|
New | 新创建状态 | 线程已被创建,还未调用start,做一些准备工作 |
Runnable | 可运行状态 | start之后进入,Runnable线程可能在运行也可能没有在运行,取决于系统分配的时间 |
Blocked | 阻塞状态 | 线程被锁阻塞,暂时不活动 |
Waiting | 等待状态 | 线程不运行任何代码,消耗最少的资源,直至调度器激活该线程 |
Timed Waiting | 超时等待状态 | 与Waiting 不同在于,可以在指定时间内返回 |
Terminated | 终止状态 | 当前线程执行完毕:可能是run运行结束,或者出现未能捕获的异常导致线程终止 |
5、线程如何从新建状态
进入可运行状态
?
Thread.start()
6、线程如何从可运行状态
到阻塞状态
- 线程在
请求锁
的时候会进入阻塞状态
- 线程一旦
得到锁
会返回到可运行状态
备注:
如下题目中的
Object.wait()
是指具体对象调用wait等方法---someObject.wait()
,Thread.join
是指具体线程调用该方法—childThread.join()
7、线程如何从可运行状态
切换到等待状态
- 进入等待:
Object.wait()---当前线程进入等待状态(当前线程需要已经获得过锁,且调用后会失去锁)、Thread.join()---父线程会等待子线程
- 退出:
Object.notify()和Object.notifyAll()
8、线程如何从可运行状态
切换到超时等待状态
- 进入:
Thread.sleep(long)、Thread.join(long)---让父线程等待子线程,子线程结束后父线程才继续执行、Object.wait(long)
- 退出:
Object.notify()、Object.notifyAll()
或者超时退出
9、线程如何从可运行状态
切换到终止状态
- 执行完毕
- 异常退出
10、Object.notify()、Object.notifyAll()之间的区别
Object.notify()
: 随机唤醒一个wait线程
,调用该方法后只有一个线程会由等待池
进入锁池
。
Object.notifyAll()
: 会将对象等待池
中的所有线程都进入锁池
,进行竞争。竞争到的线程会继续执行,在释放掉对象锁
后,锁池中的线程会继续开始竞争。(进入到锁池的线程,不会再进入等待池)
11、等待池和锁池是什么?
等待池
:线程调用对象的wait
方法后,会释放该对象的锁,然后进入到该对象的等待池中
。锁池
:线程想要获得对象的锁,但是此时锁已经被其他线程拥有,这些线程就会进入到该对象的锁池
中。
12、创建线程的三种方法
- 继承Thread,重写run方法
- 实现Runnable接口,并实现该接口的run方法
- 实现Callable接口(Executor框架中的内容,优势是能在任务结束后提供一个返回值,Runnable无法这样做),重写call方法。
- 推荐第二种
Runnable接口
的方法,因为继承Thread没有必要。
13、终止线程的两种方法
- 调用
Thread.interrupted()
设置中断标志位,并通过Thread.currentThread().isInterrupted()
检查标志位。缺点:被中断的线程不一定会终止- 在
run()
方法中设置boolean标志位(需要volatile
修饰为易变变量):条件满足时跳出循环,run运行结束,thread安全终止
同步
14、重入锁是什么?(3)
- 重入锁ReentrantLock在Java SE 5.0引入
- 该锁支持一个线程对资源的重复加锁
- 一个线程在锁住锁对象后,其他任何线程都无法进入Lock语句
val mLock = ReentrantLock()
mLock.lock()
try {
//需要同步的操作
}finally {
mLock.unlock() //finally中进行解锁,避免死锁问题
}
15、可重入锁的用途?
阻塞队列
就是使用ReentrantLock
实现的。
16、条件对象/条件变量的作用(4)
- 用于管理那些获得锁却因部分条件不满足而无法正常工作的线程
- 可以通过
newCondition
获得锁lock
的条件变量(和ReentrantLock配合使用
)- 条件对象调用
await
方法,当前线程就会阻塞并且放弃该锁await
线程会进入阻塞状态
,直到另一个线程,调用同一条件对象的signalAll()
方法,之后等待的所有线程通过竞争条件去抢锁
//1. 可重入锁
val mLock = ReentrantLock()
mLock.lock()
//2. 条件变量
val condition = mLock.newCondition()
try {
while(条件不满足){
//3. await进入Block状态
condition.await()
}
//4. 条件满足方会进行后续操作
//...
//5. 操作完成后调用同一条件变量的signalAll去激活等待该条件的线程
condition.signalAll()
}finally {
mLock.unlock() //finally中进行解锁,避免死锁问题
}
17、synchronized同步方法(4)
Lock
和condition
提供了高度的锁定控制,然而大多数情况下不需要这样麻烦- 从Java 1.0开始,每个对象都有一个内部锁
- 当一个方法使用
synchronized
修饰,意味着线程必须获得内部锁,才能调用该方法
synchronized public void doSth() throws InterruptedException{
//1. 条件不满足,进入Block状态
while(条件不满足){
wait();
}
//2. 条件满足方会进行后续操作
//...
//3. 解除该锁,并通知所有阻塞的线程
notifyAll();
}
- 备注:Kotlin学的不深,暂时没找到Kotlin中同步的方法,就用Java实现
18、同步代码块的使用(1)和问题(2)
- java中可以通过给一个Object对象上锁,来使用代码块
- 同步代码块非常脆弱不推荐
- 一般实现同步,最好使用
java.util.concurrent
包下提供的类,例如阻塞队列
Object object = new Object();
synchronized (object){
//进行处理, 不推荐使用
}
19、synchronized方法和synchronized同步代码块的区别?
用synchronized修饰符
修饰的方法就是同步方法
synchronized代码块
需要一个对象锁
20、死锁是什么?
死锁
是指两个或者两个以上线程/进程
进行资源竞争时出现了阻塞现象,如果没有外力帮助,它们都将无法继续工作下去,此时系统处于死锁状态
。
21、可能出现死锁的场景
可重入锁ReentrantLock
在mLock.lock()
后如果出现异常,但是没有在try-catch
的finally
中没有执行mLock.unLock()
就会导致死锁。notify
比notifyAll
更容易出现死锁
Java中的volatile
22、Java的堆内存是什么?(2)
- 堆内存用于存储
对象实例
- 堆内存被所有线程所共享: 会存在内存可见性问题
23、 Java中的局部变量、方法定义的参数是否会被线程所共享?
- 局部变量、方法定义的参数则不会在线程间共享:不存在内存可见性问题,也不会受到内存模型影响,
24、Java内存模型的作用
- Java内存模型控制线程之间的通信,
决定了
一个线程对共享内存的写入何时对另一个线程可见。定义了线程和主存之间的抽象关系:
- 线程之间的共享变量存储在
主存
中,每个线程都有一个私有的本地内存
,本地内存中存储了该该线程贡献变量的副本,本地内存是Java内存模型中的抽象概念
,实际上并不存在,覆盖了缓存、写缓存区、寄存器
等区域。
24、Java内存模型图
graph TD;
threadA(线程A);
threadB(线程B);
threadAM(本地内存);
threadBM(本地内存);
threadAV(共享变量副本A);
threadBV(共享变量副本B);
mainM(主内存_共享变量);
threadA-->threadAM;
threadAM-->threadAV;
threadAV-->mainM;
threadB-->threadBM;
threadBM-->threadBV;
threadBV-->mainM;
25、线程间共享变量的通信需要满足的步骤?(2)
A、B线程之间数据通信,需要满足两个步骤:
1. 第一步线程A将本地更新过的共享数据刷新到主存中;
2. 第二步线程B到主存中读取已经刷新的最新共享数据
26、可见性是什么?
- 可见性是指线程修改的状态能否立即对另一个线程可见
volatile
修饰的变量,在发生变化后会立即更新到主存,已保证共享数据的可见性
27、volatile关键字的作用
- 能保证有序性:禁止指令重新排序,之前的指令不会在volatile之后执行,之后指令也不会在之前执行
- 保证可见性:更新的数据立即可见
- 不保证原子性
28、有序性是什么?(2)
- Java中
编译器和处理器
能对指令的顺序重新排序,可不会影响单个线程执行的正确性,却无法保证多线程并发的正确性。保证多线程并发的正确性
就需要保证有序性
29、如何能保证有序性?(2)
1.
volatile
能保证有序性
2.synchronized和Lock
也能保证有序性
30、原子性是什么?
- 对基本数据类型变量的赋值和读取是原子性操作-要么不执行,要么不会被中断。
x = 3; //原子操作
y = x; //非原子操作:复制,并且存储
x++; //非原子操作: 读取x,自加,存储
31、如何保证操作的原子性?
java,util.concurrent.atomic
包中很多类使用高效的机器级别指令来保证操作的原子性AtomicInteger
的incrementAndGet/decrementAndGet()
提供原子性自加/自减-可以作为共享计数器而无需同步AtomicBoolean、AtomicLong、AtomicReference
等类也都是原子性操作- 原子性操作类应该由开发并发工具的程序员使用,而不是应用程序员使用
32、使用volatile的典型场景
- 状态标志:如线程run方法中通过标志位判断是否终止线程,就比使用synchonized要简单和高效(
通过synchronized也可以实现,但是该场景使用volatile性能更高
)- 双重检查模式(DCL): 应用于单例模式的getInstance保证实例唯一。DLC资源利用率高,第一次加载反应稍慢,在高并发情况下有一定缺陷
33、部分场景下使用volatile取代synchronized的要点
synchronized
能提供同步保护,却会影响性能。一定场景下可以用volatile
替换。volatile
无法保证原子性,必须具备两个条件才可以替换- 条件1:对变量的写操作不依赖于当前值(不能自增、自减)
- 条件2:该变量没有包含在具有其他变量的不等式中(例如,volatile a,b, 不等式a
阻塞队列
34、什么是阻塞队列
- 阻塞队列常应用于生产者-消费者模型
- 阻塞队列需要满足1:队列中没有数据时,消费者端的所有线程全部自动阻塞(挂起)
- 阻塞队列需要满足2:队列中填满数据时,生产者端的所有线程都自动阻塞
35、阻塞队列(BlockingQueue)核心方法
- 放入数据:
- offer(object),可以存入,返回true;不可以存入,返回false。该方法不会阻塞当前线程
- put(Object), 阻塞队列有空间,则存入;没有空间,当前线程阻塞,直至阻塞队列有空间存放数据。
- 获取数据:
- poll(time):从阻塞队列中将首位对象取出。若不能取出,再等待time的时间。取不到就返回null
- take():取走队列中首位数据。若队列为空,则当前线程阻塞,直到队列中有数据,并且返回该数据。
- drainTo(): 一次性取走所有可用数据。无需多次加锁操作,提高效率。
36、Java中7种阻塞队列的要点
种类 | 特点 | 备注 |
---|---|---|
ArrayBlockingQueue | 数组组成的有界阻塞队列 | 默认不保证线程公平地访问队列 |
LinkedBlockingQueue | 链表组成的有界阻塞队列 | 若构造时不指定队列缓存区大小,默认无穷大。一旦生产速度>消费速度,会导致内存耗尽 |
PriorityBlockingQueue | 支持优先级排序的无界阻塞队列—默认升序排列 | 能通过compareTo 方法和构造参数comparator 对元素排序,但无法保证同级元素的顺序 |
DelayQueue | 延时获取元素的无界阻塞队列 | 每个元素必须实现Delayed接口,创建时指定元素到期时间,元素到期后才能取出 |
SynchronousQueue | 不存储元素的阻塞队列 | |
LinkedTransferQueue | 链表存储的无界阻塞队列 | |
LinkedBlockingDeque | 链表存储的双向阻塞队列 | 可以从两端同时插入和删除,减少一半竞争 |
37、阻塞队列ArrayBlockingQueue实现原理
- 内部维护一个Object类型的数组
- lock所采用的可重入锁(ReentrantLock)
38、ArrayBlockingQueue的put()源码解析和要点
/**
* 存放数据
* 1-有空间存放,就直接存入数据
* 2-没有空间存放,当前线程阻塞到有多余空间,再存入
*/
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
final ReentrantLock lock