Android 进程与线程(了解>使用>常见面试题)
Posted 冬天的毛毛雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 进程与线程(了解>使用>常见面试题)相关的知识,希望对你有一定的参考价值。
🔥 概念
💥 进程
-
程序运行的基本单元。
-
系统资源分配和调度的基本单位。
-
有自己独立的地址空间。
-
多个进程可并发执行。
-
线程的容器
💥 线程
-
程序执行的最小单位。
-
CPU调度和分派的基本单位。
-
没有独立的地址空间,多个线程共享地址空间。
-
多个线程可并发执行,某一个线程可以创建和撤销另外的线程。
💥 进程和线程的区别
-
地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
-
资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、CPU等,但是进程之间的资源是独立的。
-
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以使用多进程可以保证其他模块的正常运行。
-
进程切换时,消耗的资源大,效率不高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
-
执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
-
都可以并发执行。
-
一个程序至少有一个进程,一个进程至少有一个线程。
可以将系统看成一个工厂,进程就是车间,多进程就是多个车间,线程就是流水线,多进程就是多个车间,多线程就是多个流水线。
🔥 android中的进程
💥 进程
当应用程序组件启动并且该应用程序没有任何其他组件在运行时,Android 系统会为该应用程序启动一个新的 Linux 进程,并使用单个执行线程。 默认情况下,同一应用程序的所有组件都在同一进程和线程(称为“主”线程)中运行。
💥 进程的等级(生命周期)
🌀 前台进程(Foreground process)
它表明用户正在与该进程进行交互操作优先级是最高的。Android系统-依据下面的条件来将一个进程标记为前台进程:
-
有一个Activity且正在执行**onResume()**方法(用户正在与其交互)。
-
有一个Service且正在执行(onCreate()、onStart()、onDestroy())之一的方法。
-
有一个BroadcastReceiver且正在执行 onReceive() 方法。
🌀 可见进程(Visible process)
它表明虽然该进程没有持有任何前台组件,但是它还是能够影响到用户看得到的界面。android系统依据下面的条件将一个进程标记为可见进程:
-
有一个Activity且它不在交互,但仍可见(其 onPause() 方法已被调用)。例如,当一个activity启动了一个dialog,这个activity就被对话框挡在后面。
-
有一个**Service正在执行Service.startForeground()**的方法。
-
托管系统用于用户知道的特定功能的服务,例如动态壁纸、输入法服务等。
🌀 服务进程(Service process)
持有已使用 startService() 方法启动的Service。 虽然这些进程对用户来说并不直接可见,但它们一般都在做用户关心的事情(如后台网络数据上传或下载)
🌀 缓存进程(Cached process)
缓存进程是当前不需要的进程,因此当其他地方需要内存等资源时,系统可以根据需要随意终止它。
- 持有一个或多个不可见Activity(调用了onStop()方法)。通常情况下都会有很多后台进程,当内存不足的时候,在所有的后台进程里面,会按照LRU(最近使用)规则,优先回收最长时间没有使用过的进程。
在决定如何对流程进行分类时,系统将根据在流程中当前活动的所有组件中找到的最重要的级别来做出决定。
进程的优先级也可以基于进程对它的其他依赖性而增加。
💥 多进程
默认情况下,同一应用程序的所有组件都在同一进程中运行,大多数应用程序不应更改这一点。但是,如果你发现需要控制某个组件属于哪个进程,则可以在 < application> 中进行。
每种类型的组件元素 (< activity >、< service>、< receiver> 和 < provider>) 的清单条目都支持 android:process 属性,该属性可以指定该组件应在其中运行的进程。
< application> 元素还支持 android:process 属性。
Android 的一个不同寻常的基本特性是应用程序进程的生命周期不受应用程序本身直接控制。相反,它是由系统通过系统知道正在运行的应用程序部分的组合、这些东西对用户的重要性以及系统中可用的总内存量来确定的。
默认进程就是主进程。其他进程一般来说都是子进程。
如:咱们用到的微信,他那么多功能肯定不是在一个默认进程里面操作的。使用多进程,即使某个功能因为线程问题导致进程崩溃也崩溃一个进程,其他非该进程的功能可以正常使用。
🌀 多进程产生多个Application
如果注册的四大组件中的任意一个组件时用到了多进程,运行该组件时,都会创建一个新的Application对象。对于多进程重复创建Application这种情况,只需要在该类中对当前进程加以判断即可。
com.scc.demo(12095):com.scc.demo进程名;12095进程id
代码实现:AndroidMainfest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.scc.demo">
<application
android:name=".SccApp"
...
android:theme="@style/Theme.Demo">
<activity android:name=".actvitiy.MainActivity"
>
...
</activity>
<activity android:name=".actvitiy.TouchActivity"
android:process="com.scc.touch.wudi"/>
<activity android:name=".actvitiy.ViewActivity"
android:process=":view"/>
...
</application>
</manifest>
根据默认进程名和当前进程名比较是否进行初始化。
public class SccApp extends Application {
@RequiresApi(api = Build.VERSION_CODES.P)
@Override
public void onCreate() {
super.onCreate();
getProcessName(BuildConfig.APPLICATION_ID);
}
public void getProcessName(String processName){
ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> processInfos = activityManager.getRunningAppProcesses();
if(processInfos!=null)
{
for(ActivityManager.RunningAppProcessInfo processInfo:processInfos){
MLog.e(processInfo.processName);
if(processName.equals(processInfo.processName)){
init();
}
}
}
}
//初始化
private void init(){
//
CrashReport.initCrashReport(getApplicationContext(), "注册时申请的APPID", false);
}
}
💥 进程间通信
-
Bundle
-
文件共享
-
AIDL
-
Messenger
-
Content Provider
-
Socket
这个内容太多,咱在后面的文章再详细描述。
🔥 Android中的线程
线程分为两种:
-
UI/Main Thread (主线程)
-
Worker Thread(工作线程)
一个线程总是由另一个线程启动,所以总有一个特殊的线程,叫做主线程。它是应用启动并执行的第一个线程。每次启动一个新工作线程,都会从主线程分出一条独立的线。
💥 UI/Main Thread (主线程)
启动应用程序时,系统会为应用程序创建一个执行线程,称为 “main”。该线程非常重要,因为它负责将事件发送到适当的用户界面小部件,包括绘图事件。与Android UI toolkit (来自Android.widget和Android.view包的组件)交互的线程。
因此,主线程有时也称为UI线程。但是,在特殊情况下,应用程序的主线程可能不是它的UI线程。
使用线程注解时,注意:构建工具将@MainThread和 @UiThread注释视为可互换的,因此你可以@UiThread 从@MainThread方法中调用方法,反之亦然。但是,在系统应用程序在不同线程上具有多个视图的情况下,UI 线程可能与主线程不同。因此,你应该 @UiThread 使用 @MainThread.
在同一进程中运行的所有组件都在UI线程中实例化。
此外,Android UI toolkit不是线程安全的。因此,你不能从工作线程操作UI—你必须从UI线程对用户界面执行所有操作。因此,Android的单线程模型只有两条规则:
-
不要阻塞UI线程;
-
不要在非UI线程访问 UI 。
🌀 阻塞UI线程
如果所有事情都发生在UI线程中,那么执行长时间操作(如网络访问或数据库查询)将阻塞整个UI。
发生ANR的原因:
-
Activity超过5秒无响应;
-
BroadcastReceiver超过10秒无响应。
🌀 Worker Thread操作UI
@Override
protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread);
//Worker Thread(工作线程)
new Thread(new Runnable() {
@Override
public void run() {
//操作UI线程
Toast.makeText(ThreadActivity.this,"我是Worker Thread",Toast.LENGTH_SHORT).show();
}
}).start();
}
运行后直接报错:
2021-10-12 14:47:47.495 4122-4247/com.scc.demo E/AndroidRuntime: FATAL EXCEPTION: Thread-7
Process: com.scc.demo, PID: 4122
java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()
at android.widget.Toast$TN.<init>(Toast.java:895)
at android.widget.Toast.<init>(Toast.java:205)
at android.widget.Toast.makeText(Toast.java:597)
at android.widget.Toast.makeText(Toast.java:566)
at com.scc.demo.actvitiy.ThreadActivity$1.run(ThreadActivity.java:18)
at java.lang.Thread.run(Thread.java:919)
💥 Worker Thread(工作线程)
因不能阻塞主线程,但是有些耗时操作(如加载图片、网络请求等)非即时相应的则可以通过工作线程来执行。
注意,你不能从UI线程或"主"线程以外的任何线程更新UI。
为了解决这个问题,Android提供了几种从其他线程访问UI线程的方法:
-
Activity.runOnUiThread(Runnable)
-
View.post(Runnable)
-
View.postDelayed(Runnable, long)
🌀 样例:子线程访问UI线程
public class ThreadActivity extends ActivityBase{
TextView tvName;
@Override
protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread);
tvName = findViewById(R.id.tv_name);
tvName.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
csThread();
startThread();
}
});
}
private void csThread(){
//Worker Thread(工作线程)
new Thread(new Runnable() {
@Override
public void run() {
//这样写直接报错
tvName.setText("我是Worker Thread---行路难!行路难!");
// ------强大的分割线------
// 下面几种方式都没问题
//第一种:Activity.runOnUiThread(Runnable)
runOnUiThread(new Runnable() {
@Override
public void run() {
tvName.setText("我是Worker Thread---行路难!行路难!");
Toast.makeText(ThreadActivity.this,"我是Worker Thread",Toast.LENGTH_SHORT).show();
}
});
//第二种:View.post(Runnable)
tvName.post(new Runnable() {
@Override
public void run() {
tvName.setText("我是Worker Thread---行路难!行路难!");
Toast.makeText(ThreadActivity.this,"我是Worker Thread",Toast.LENGTH_SHORT).show();
}
});
//第三种:View.postDelayed(Runnable, long)
tvName.postDelayed(new Runnable() {
@Override
public void run() {
tvName.setText("我是Worker Thread---行路难!行路难!");
Toast.makeText(ThreadActivity.this,"我是Worker Thread",Toast.LENGTH_SHORT).show();
}
},1000);
//第四种:Handler(下面有源码,都基于Handler来做的)
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
tvName.setText("我是Worker Thread---行路难!行路难!");
Toast.makeText(ThreadActivity.this,"我是Worker Thread",Toast.LENGTH_SHORT).show();
}
});
}
}).start();
}
}
子线程直接操作主线程报错信息:
理论上应该拿 3.1.2 Worker Thread 操作UI 时的报错信息。既然都能通过这种方式解决,就多举一个。
2021-10-12 16:02:51.754 8635-8676/com.scc.demo E/AndroidRuntime: FATAL EXCEPTION: Thread-2
Process: com.scc.demo, PID: 8635
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8798)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1606)
at android.view.View.requestLayout(View.java:25390)
...
at android.widget.TextView.checkForRelayout(TextView.java:9719)
at android.widget.TextView.setText(TextView.java:6311)
...
at com.scc.demo.actvitiy.ThreadActivity$2.run(ThreadActivity.java:31)
at java.lang.Thread.run(Thread.java:923)
🌀 几种方法源码
//Activity.runOnUiThread(Runnable)
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
//View.post(Runnable)
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
//View.postDelayed(Runnable, long)
public boolean postDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.postDelayed(action, delayMillis);
}
getRunQueue().postDelayed(action, delayMillis);
return true;
}
你会发现他们都是使用 Handler 来完成的。所以在 子线程访问UI线程 的样例中咱,可以使用 new Handler() 来完成更新 UI。
💥 线程的状态
-
new:新建状态,new出来,还没有调用start。
-
Runnable:可运行状态,调用start进入可运行状态,可能运行也可能没有运行,取决于操作系统的调度。
-
Blocked:阻塞状态,被锁阻塞,暂时不活动,阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
-
Waiting:等待状态,不活动,不运行任何代码,等待线程调度器调度,wait sleep。
-
Timed Waiting:超时等待,在指定时间自行返回。
-
Terminated:终止状态,包括正常终止和异常终止。
💥 开启线程的三种方式
-
1:继承Thread重写run方法。
-
2:实现Runnable重写run方法。
-
3:实现Callable重写call方法。
private void startThread(){
//第一种:继承Thread重写run方法
new MyThread().start();
//第二种:实现Runnable重写run方法
new Thread(new MyRunanble()).start();
//第三种:实现Callable重写call方法
FutureTask<Integer> ft = new FutureTask<Integer>(new MyCallable());
new Thread(ft).start();
}
class MyThread extends Thread{
@Override
public void run() {
MLog.e(this.getClass().getName());
}
}
class MyRunanble implements Runnable{
@Override
public void run() {
MLog.e(this.getClass().getName());
}
}
class MyCallable implements Callable {
@Override
public Object call() throws Exception {
MLog.e(this.getClass().getName());
return null;
}
}
🌀 小结
Callable 和 Runnable 类似,但是功能更强大,具体表现在:
-
可以在任务结束后提供一个返回值,Runnable不行。
-
call方法可以抛出异常,Runnable的run方法不行
-
可以通过运行Callable得到的Fulture对象监听目标线程调用call方法的结果,得到返回值,(fulture.get(),调用后会阻塞,直到获取到返回值)。
🔥 常见面试题
💥 run()和start()方法区别
-
run():方法只是线程的主体方法,和普通方法一样,不会创建新的线程。
-
start():只有调用start()方法,才会启动一个新的线程,新线程才会调用run()方法,线程才会开始执行。
💥 wait、notify、notifyAll
-
wait():释放obj的锁,导致当前的线程等待,直接其他线程调用此对象的notify()或notifyAll()方法。
-
notify(): 唤醒在此对象监视器上等待的单个线程
-
notifyAll(): 通知所有等待该竞争资源的线程
注意:当要调用wait()或notify()/notifyAll()方法时,一定要放到synchronized(obj)代码中,否则会报错java.lang.IllegalMonitorStateException。当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此等待线程虽被唤醒,但仍无法获得obj锁,直到调用线程退出synchronized块,释放obj锁后,其他等待线程才有机会获得锁继续执行。
💥 join、sleep、wait
-
join():方法在等待的过程中释放对象锁。
-
sleep():方法在睡眠时不释放对象锁,
-
wait():释放对象锁
💥 线程阻塞
-
1:线程执行了Thread.sleep(int millsecond)方法,放弃CPU,睡眠一段时间,一段时间过后恢复执行;
-
2:线程执行一段同步代码,但无法获得相关的同步锁,只能进入阻塞状态,等到获取到同步锁,才能恢复执行;
-
3:线程执行了一个对象的wait()方法,直接进入阻塞态,等待其他线程执行notify()/notifyAll()操作;
-
4:线程执行某些IO操作,因为等待相关资源而进入了阻塞态,如System.in,但没有收到键盘的输入,则进入阻塞态。
-
5:线程礼让,Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或更高优先级的线程,但并不会使线程进入阻塞态,线程仍处于可执行态,随时可能再次分得CPU时间。
-
6:线程自闭,join()方法,在当前线程调用另一个线程的join()方法,则当前线程进入阻塞态,直到另一个线程运行结束,当前线程再由阻塞转为就绪态。
-
7:线程执行suspend()使线程进入阻塞状态,必须resume()方法被调用,才能使线程重新进入可执行状态。
💥 线程中断
使用 interrupt()中断,但调用 interrupt()方法只是传递中断请求消息,并不代表要立马停止目标线程。然后通过抛出InterruptedException来唤醒它。
public class Thread {
// 中断当前线程
public void interrupt();
// 判断当前线程是否被中断
public boolen isInterrupt();
// 清除当前线程的中断状态,并返回之前的值
public static boolen interrupted();
}
💥 线程池ThreadPoolExecutor
线程池的工作原理:线程池可以减少创建和销毁线程的次数,从而减少系统资源的消耗。
当一个任务提交到线程池时:
-
1:首先判断核心线程池中的线程是否已经满了,如果没满,则创建一个核心线程执行任务,否则进入下一步。
-
2:判断工作队列是否已满,没有满则加入工作队列,否则执行下一步。
-
3:判断线程数是否达到了最大值,如果不是,则创建非核心线程执行任务,否则执行饱和策略,默认抛出异常。
💥 线程池的种类
FixedThreadPool:可重用固定线程数的线程池,只有核心线程,没有非核心线程,核心线程不会被回收,有任务时,有空闲的核心线程就用核心线程执行,没有则加入队列排队。
SingleThreadExecutor:单线程线程池,只有一个核心线程,没有非核心线程,当任务到达时,如果没有运行线程,则创建一个线程执行,如果正在运行则加入队列等待,可以保证所有任务在一个线程中按照顺序执行,和FixedThreadPool的区别只有数量。
CachedThreadPool:按需创建的线程池,没有核心线程,非核心线程有Integer.MAX_VALUE个,每次提交任务如果有空闲线程则由空闲线程执行,没有空闲线程则创建新的线程执行,适用于大量的需要立即处理的并且耗时较短的任务。
ScheduledThreadPoolExecutor:继承自ThreadPoolExecutor,用于延时执行任务或定期执行任务,核心线程数固定,线程总数为Integer.MAX_VALUE。
💥 如何保证线程安全
线程安全性体现在:
-
原子性:提供互斥访问,同一时刻只能有一个线和至数据进行操作。
JDK中提供了很多atomic类,如AtomicInteger\\AtomicBoolean\\AtomicLong,它们是通过CAS完成原子性。
JDK提供锁分为两种:
- synchronized依赖JVM实现锁,该关键字作用对象的作用范围内同一时刻只能有一个线程进行操作。
- 另一种是LOCK,是JDK提供的代码层面的锁,依赖CPU指令,代表性是ReentrantLock。
-
可见性:一个线程对主内存的修改及时被其他线程看到。
JVM提供了synchronized和volatile,volatile的可见性是通过内存屏障和禁止重排序实现的,volatile会在写操作时,在写操作后加一条store屏障指令,将本地内存中的共享变量值刷新到主内存;会在读操作时,在读操作前加一条load指令,从内存中读取共享变量。
-
有序性:指令没有被编译器重排序。
可通过volatile、synchronized、Lock保证有序性。
💥 volatile、synchronized、Lock、ReentrantLock 区别
-
volatile:解决变量在多个线程间的可见性,但不能保证原子性,只能用于修饰变量,不会发生阻塞。volatile能屏蔽编译指令重排,不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。多用于并行计算的单例模式。volatile规定CPU每次都必须从内存读取数据,不能从CPU缓存中读取,保证了多线程在多CPU计算中永远拿到的都是最新的值。
-
synchronized:互斥锁,操作互斥,并发线程过来,串行获得锁,串行执行代码。解决的是多个线程间访问共享资源的同步性,可保证原子性,也可间接保证可见性,因为它会将私有内存和公有内存中的数据做同步。可用来修饰方法、代码块。会出现阻塞。synchronized发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。非公平锁,每次都是相互争抢资源。
-
lock:是一个接口,lock可以让等待锁的线程响应中断。在发生异常时,如果没有主动通过unLock()去释放锁,则可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。
-
ReentrantLoc:可重入锁,锁的分配机制是基于线程的分配,而不是基于方法调用的分配。ReentrantLock有tryLock方法,如果锁被其他线程持有,返回false,可避免形成死锁。对代码加锁的颗粒会更小,更节省资源,提高代码性能。ReentrantLock可实现公平锁和非公平锁,公平锁就是先来的先获取资源。ReentrantReadWriteLock用于读多写少的场合,且读不需要互斥场景。
💥 Thread为什么不能用stop方法停止线程
从官方文档可以得知,调用Thread.stop()方法是不安全的,这是因为当调用Thread.stop()方法时,会发生下面两件事:
-
1:即刻抛出ThreadDeath异常,在线程的run()方法内,任何一点都有可能抛出ThreadDeath Error,包括在catch或f
以上是关于Android 进程与线程(了解>使用>常见面试题)的主要内容,如果未能解决你的问题,请参考以下文章