Android 进程+线程

1.进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
进程一般指一个执行单元,在移动设备上就是一个程序或应用。当一个应用开始运行时,系统会为它创建一个进程,一个应用默认只有一个进程,这个进程的名称就是应用的包名。Android中所说的多进程一般指一个应用包含多个进程。之所以要使用多进程有两方面原因:某些模块由于特殊的需求要运行在单独的进程;增加应用可用的内存空间。

进程的特点:
①进程是系统资源和分配的基本单位,而线程是调度的基本单位;
②每个进程都有自己独立的资源和内存空间;
③其它进程不能任意访问当前进程的内存和资源;
④系统给每个进程分配的内存会有限制;

进程按优先级可以分为五类,优先级从高到低排列:
①前台进程:该进程包含正在与用户进行交互的界面组件,如Activity。在接收关键生命周期方法时会让一个进程临时提升为前台进程,包括任何服务的生命周期方法onCreate()和onDestroy()和任何广播接收器onReceive()方法。这样做确保了这些组件的操作是有效的原子操作,每个组件都能执行完成而不被杀掉。
②可见进程:该进程中的组件虽然没有和用户交互,但是仍然可以被看到。activity可见的时候不一定在前台。比如前台的activity使用对话框启动一个新的activity或者一个透明activity 。另一个例子是当调用运行时权限对话框时(事实上它就是一个 activity)。
③服务进程:该进程包含在执行后台操作的服务组件,比如播放音乐的Service。对于许多在后台做处理而没有立即成为前台服务的应用都属于这种情况。
④后台进程:该进程包含的组件没有与用户交互,用户也看不到 Service。在一般操作场景下,设备上的许多内存就是用在这上面的,使可以重新回到之前打开过的某个 activity 。
⑤空进程:没有任何界面组件、服务组件,或触发器组件,只是出于缓存的目的而被保留(为了更加有效地使用内存而不是完全释放掉),只要 Android 需要可以随时杀掉它们。

Android开启多进程只有一种方法,就是在AndroidManifest.xml中注册Service、Activity、Receiber、ContentProvider时指定"android:process”属性。命名之后就成了一个单独的进程。
android:process=":remote">
android:process=“com.shh.ipctest.remote2”
进程分私有进程和全局进程:
①私有进程的名称前面有冒号,它属于当前应用的私有进程,其它应用的组件不能和它跑在同一进程中。
android:process=":musicservice"
以:开头是一种简写,系统会在当前进程名前附上当前包名,完整的进程名为:com.test.demo:musicservice。
②全局进程的名称前面没有冒号。android:process=“com.test.demo.service”
这是完整的命名方式,不会附加包名,其它应用如果和该进程的ShareUID、签名相同,则可以和它跑在同一个进程,实现数据共享。

1)多进程引发的问题
①静态成员和单例模式失效;
②线程同步机制失效;
③SharedPreferences可靠性降低;
④Application被多次创建;
对于前两个问题,在Android中系统会为每个应用或进程分配独立的虚拟机,不同的虚拟机占有不同的内存地址空间,所以同一个类的对象会产生不同的副本,导致共享数据失败,必然也不能实现线程的同步。
第三个问题,由于SharedPreferences底层采用读写XML文件的方式实现,多进程并发的的读写很可能导致数据异常。
第四个问题,Application被多次创建和前两个问题类似,系统在分配多个虚拟机时相当于把同一个应用重新启动多次,必然会导致Application多次被创建,为了防止在 Application中出现无用的重复初始化,可使用进程名来过滤,只让指定进程的才进行全局初始:
public class MyApplication extends Application{
@Override\npublic void onCreate() {
super.onCreate();
String processName = “com.demo.test”;
if(getPackageName().equals( processName)) {
// do some init
}
}
}
为了节省系统内存,在退出该Activity的时候可以将其杀掉(如果没有人为杀掉该进程,在程序完全退出时该进程会被系统杀掉)

2)进程间通信方式
android SDK中提供了4种用于跨进程通讯的方式,正好对应于android系统中4种应用程序组件:Activity、Content Provider、Broadcast和Service。其中Activity可以跨进程调用其他应用程序的Activity;Content Provider可以跨进程访问其他应用程序中的数据,也可以对其他应用程序的数据进行增删改操作;Broadcast可以向android系统中所有应用程序发送广播,而需要跨进程通讯的应用程序可以监听这些广播;Service和Content Provider类似,也可以访问其他应用程序中的数据,但不同的是,Content Provider返回的是Cursor对象,而Service返回的是Java对象,这种可以跨进程通讯的服务叫AIDL服务。
①Intent(Bundle) 
②ContentProvider
③文件
两个进程可以到同一个文件去交换数据,不仅可以保存文本文件,还可以将对象持久化到文件,从另一个文件恢复。要注意的是,当并发读/写时可能会出现并发的问题。
④广播Broadcast
⑤AIDL方式
⑥Messenger

2.线程
线程是进程中可独立执行的最小单位,也是CPU资源分配的基本单位。它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一些在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

Android中常用的7种异步方式:Thread、HandlerThread、IntentService、AsyncTask、线程池、RxJava和Kotlin协程。

1)启动线程的方法
①新建一个类继承Thread,并实现它的run()方法。
class MyThread extends Thread {
@override
public void run() {
……
}
}
new MyThread().start();
注意:一定要调用start()方法而不是run方法。否则新线程不能开启。
②新建一个类继承Runnable,然后再newThread调用该类,最后调用start方法。
class MyRunnable extends Runnable {
@override
public void run() {
……
}
}
new Thread(new MyRunnable()).start();
③使用lambda表达式。
new Thread(() -> {
……
}).start();
④通过线程池启动Executors.newCachedThread

2)线程的特性
(1)线程的4个属性
线程有编号、名字、类别、优先级四个属性,此外,线程的部分属性还具有继承性。
①编号id
线程的编号用于标识不同的线程,每条线程拥有不同的编号。
注意:不能作为唯一标识,某个编号的线程运行结束后,该编号可能被后续创建的线程使用,因此编号不适合用作唯一标识,编号是只读属性,不能修改。
②名字name
每个线程都有自己的名字,名字的默认值是Thread-线程编号,比如Thread-0。当然也可以给线程设置名字,以自己定义的名字区分每一条线程。
③类别daemon
线程的类别分为守护线程和用户线程,可以通过setDaemon(true) 把线程设置为守护线程。
当JVM要退出时,它会考虑是否所有的用户线程都已经执行完毕,是的话则退出;而对于守护线程,JVM 在退出时不会考虑它是否执行完成。
作用:守护线程通常用于执行不重要的任务,比如监控其他线程的运行情况,GC线程就是一个守护线程。
注意事项:setDaemon()要在线程启动前设置,否则JVM会抛出非法线程状态异常。
④优先级Priority
线程的优先级表示希望优先运行哪个线程,线程调度器会根据这个值来决定优先运行哪个线程。
Java中线程优先级的取值范围为 1~10,默认值是5,Thread中定义了下面三个优先级常量:
最低优先级:MIN_PRIORITY = 1;
默认优先级:NORM_PRIORITY = 5;
最高优先级:MAX_PRIORITY = 10;
注意:线程调度器把线程的优先级当作一个参考值,不一定会按设定的优先级顺序执行线程。
优先级使用不当会导致某些线程永远无法执行,也就是线程饥饿的情况。
(2)线程的继承性
线程的类别和优先级属性是会被继承的,线程的这两个属性的初始值由开启该线程的线程决定。
假如优先级为5的守护线程A开启了线程 B,那么线程B也是一个守护线程,而且优先级也是5。这时就把线程A叫做线程B的父线程,把线程B叫做线程A的子线程。
(3)线程的重要方法
①start() 启动线程
该方法只能调用一次,再次调用不仅无法让线程再次执行,还会抛出非法线程状态异常。
② run()
run()方法中放的是任务的具体逻辑,该方法由JVM调用,一般情况下开发者不需要直接调用。
注意:如果开发者调用了run()方法,加上JVM也调用了一次,那这个方法就会执行两次。③Thread.currentThread()
静态方法,用于获取执行当前方法所在的线程。可以在任意方法中调用Thread.currentThread()获取当前线程,并设置它的名字和优先级等属性。
④ join()/join(long illis)
在当前线程里调用其他线程的join方法,会使当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。其他线程执行完毕或millis时间到了,当前线程进入就绪状态。
也就是说,join()方法用于等待其他线程执行结束。如果在线程A中调用了线程B的join()方法,那线程A会进入等待状态,直到线程B运行结束。
注意:join()方法导致的等待状态是可以被中断的,所以调用这个方法需要捕获中断异常。
⑤Thread.yield()
静态方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。
作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。
⑥ Thread.sleep(ms)
静态方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis时间后线程自动苏醒进入就绪状态。
sleep()方法是给其他线程执行机会的最佳方式。
⑦interrupt()
在当前线程里调用其他线程的interrupt()方法,可中断指定的线程。如果指定的线程调用了wait方法组或join方法在阻塞状态,那么指定的线程就会抛出InterruptedException。
⑧Thread.interrupted
静态方法,检查当前线程是否被设置了中断,该方法会重置当前线程的中断标志,返回当前线程是否被设置了中断。
⑨isInterrupted()
当前线程里调用其他线程的isInterrupted()方法,返回指定线程是否被中断。
⑩Object.wait()
在当前线程里调用对象的wait()方法,当前线程会释放对象锁,进入等待队列。依靠notify()或notifyAll()唤醒或wait(long millis)millis时间到自动唤醒。
(4)线程的六种状态
线程的生命周期不仅可以由开发者触发,还会受到其他线程的影响,下面是线程各个状态之间的转换示意图:
在这里插入图片描述
通过Thread.getState()可以获取线程的状态,该方法返回的是一个枚举类Thread.State。
线程的状态有新建、可运行、阻塞、等待、限时等待和终止6 种,这6种状态之间的转换过程:
①新建状态NEW:当一个线程创建后未启动时,它就处于新建状态。
②可运行状态RUNNABLE:当调用线程的start()方法后,线程就进入了可运行状态,可运行状态又分为就绪(READY)和运行(RUNNING)状态。
就绪状态:处于就绪状态的线程可被线程调度器调度,调度后线程的状态会从就绪转换为运行状态,处于就绪状态的线程也叫活跃线程。当线程的yield()方法被调用后,线程的状态可能由运行状态变为预备状态。
运行状态:运行状态表示线程正在运行,也就是处理器正在执行线程的run()方法。
线程对象创建后,其他线程调用该对象的start()方法,则该线程位于可运行线程池中,等待被线程调度器选中获取CPU的使用权,此时处于就绪状态。就绪状态的线程在获得CPU时间片后变为运行状态。
③阻塞状态BLOCKED:线程阻塞于锁。当下面几种情况发生时线程就处于阻塞状态:发起阻塞式 I/O操作、申请其他线程持有的锁、进入一个synchronized方法或代码块失败。
④等待状态WAITING:一个线程执行特定方法后会等待其他线程执行执行完毕,此时线程进入等待状态。
下面的几个方法可以让线程进入等待状态:
Object.wait()
LockSupport.park()
Thread.join()
进入等待状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
⑤限时等待状态TIMED_WAITING:等待一段时间,时间到了之后就会转换为可运行状态。
下面几个方法可以让线程进入限时等待状态:
Thread.sleep(ms);
Thread.join(ms);
Object.wait(ms);
LockSupport.parkNonos(ns);
LockSupport.parkUntil(time);
⑥终止状态TERMINATED:当线程的任务执行完毕或任务执行遇到异常时,线程就处于终止状态。
(5)线程的调度原理
①Java内存模型
Java内存模型规定了所有变量都存储在主内存中,每条线程都有自己的工作内存。
所以线程每次对数据操作,这些数据都是当前线程工作内存中的共享变量副本,而不是直接在主内存操作。如果线程对变量的操作没有刷回主内存,仅仅改变了自己工作内存的变量副本,那么对于其他线程来说是不可见的,其他线程不知道这个变量发生了变化。而如果一个变量没有读取内存中的新值,而是使用旧值去做后续操作的话,会得到错误的结果,这就体现了线程安全问题——可见性。
在这里插入图片描述
②高速缓存
在这里插入图片描述
现代处理器的处理能力远胜于主内存(DRAM)的访问速率,为了弥补处理器与主内存之间的差距,硬件设计者在主内存与处理器之间加入了高速缓存。处理器执行内存读写操作时,不是直接与主内存打交道,而是通过高速缓存进行的。
高速缓存相当于是一个由硬件实现的容量极小的散列表,这个散列表的key是一个对象的内存地址,value可以是内存数据的副本,也可以是准备写入内存的数据。
③Java线程调度机制
在任意时刻CPU只能执行一条机器指令,每个线程只有获取到CPU的使用权后才可以执行指令。所以在任意时刻只有一个线程占用CPU处于运行的状态。(多线程并发运行实际上指多个线程轮流获取CPU使用权,分别执行各自的任务)
线程的调度由JVM负责,按照特定的机制为多个线程分配CPU的使用权。
线程调度模型分为两类:分时调度模型和抢占式调度模型。
(1)分时调度模型
分时调度模型是让所有线程轮流获取CPU使用权,并且平均分配每个线程占用CPU的时间片。
(2)抢占式调度模型
JVM采用的是抢占式调度模型,也就是让优先级高的线程先占用CPU,如果线程的优先级都一样,那就随机选择一个线程占用CPU。所以如果同时启动多个线程,并不能保证它们能轮流获取到均等的时间片。如果程序想干预线程的调度过程,最简单的办法就是给每个线程设定一个优先级。
(6)线程的四个活跃性问题
①死锁
在这里插入图片描述
死锁是一种常见多线程活跃性问题,如果两个或更多的线程因为相互等待对方而被永远暂停,就叫死锁现象。
②死锁产生的四个条件
(1)资源互斥:涉及的资源必须是独占的,也就是资源每次只能被一个线程使用。
(2)资源不可抢夺:涉及的资源只能被持有该资源的线程主动释放,无法被其他线程抢夺(被动释放)。
(3)占用并等待资源:涉及的线程至少持有一个资源,还申请了其他资源,而其他资源刚好被其他线程持有,并且线程不释放已持有资源。
(4)循环等待资源:涉及的线程必须等待别的线程持有的资源,而别的线程又反过来等待该线程持有的资源。
只要产生了死锁,上面的条件就一定成立,但是上面的条件都成立也不一定会产生死锁。
③避免死锁的三个方法
要想消除死锁,只要破坏掉上面的其中一个条件即可。由于锁具有排他性,且无法被动释放,所以只能破坏掉第三个和第四个条件:
(1)粗锁法
使用粗粒度的锁代替多个锁,锁的范围变大了,访问共享资源的多个线程都只需要申请一个锁,因为每个线程只需要申请一个锁就可以执行自己的任务,这样“占用并等待资源”和“循环等待资源”这两个条件就不成立了。
粗锁法的缺点是会降低并发性,而且可能导致资源浪费,因为采用粗锁法时,一次只能有一个线程访问资源,这样其他线程就只能搁置任务了。
(2)锁排序法
锁排序法指相关线程使用全局统一的顺序申请锁。
假如有多个线程需要申请锁,只需要让这些线程按照一个全局统一的顺序去申请锁,这样就能破坏“循环等待资源”这个条件;
(3)tryLock
显式锁ReentrantLock.tryLock(long timeUnit)方法允许为申请锁的操作设置超时时间,这样就能破坏“占用并等待资源”这个条件。
(4)开放调用
开放调用就是一个方法在调用外部方法时不持有锁,开放调用能破坏“占用并等待资源”这个条件。
(7)终止线程的方法
线程结束提供了stop()方法;但已经不推荐使用了,因为thread.stop()会立即将线程终止,导致代码逻辑不完整。比如在子线程里设置休眠1s之后执行某操作,但是主线程在休眠了0.1s之后就执行子线程的stop()方法,就导致子线程里面的逻辑不完整。另外此线程持有的锁也会立即释放,导致其他线程可能会使用到不完整的数据。
因此停止线程只有一种方法,就是run方法结束。开启多线程执行时,代码通常是使用循环结构,最多的就是while循环,所以只要控制住循环就可以让run方法结束,也就是线程结束。

终止线程方法:
①设置一个标记,当想要终止线程时修改标记位即可。
②借助thread的interrupt()方法,将线程的中断标识位设为true,线程会不时地检查这个中断标识位以判断线程标识位是否被置位。
可以调用Thread.currentThread().isInterrupted()方法获得线程是否被中断。但是如果线程被阻塞就无法判断线程中断状态,因为一个线程处于阻塞状态,线程在检测中断标识位时如果发现中断标识位为true,会在阻塞方法调用处抛出InterruptedException异常,并且在抛出异常之前将线程的中断标识位重新设置为false。所以如果捕获到InterruptedException异常,就需要做出相应的操作。
(8)线程间通信
包括主线程和子线程之间的通信、子线程之间的通信两种。
主线程与子线程之间的通信方式:
①AsyncTask机制  
AsyncTask异步任务,也就是说在UI线程运行的时候可以在后台执行一些异步的操作;AsyncTask可以很容易且正确地使用UI线程,AsyncTask允许进行后台操作,并在不显示使用工作线程或Handler机制的情况下,将结果反馈给UI线程。
但是AsyncTask只能用于短时间的操作(最多几秒就应该结束的操作),如果需要长时间运行在后台,就不适合使用AsyncTask了,只能去使用Java提供的其他API来实现。
②Handler机制
Handler继承自Object类,用来发送和处理Message对象或Runnable对象。
Handler在创建时会与当前所在的线程的Looper对象相关联(如果当前线程的Looper为空或不存在,则会抛出异常,此时需要在线程中主动调用Looper.prepare()来创建一个Looper对象)。
使用Handler的主要作用就是在后面的过程中发送和处理Message对象和让其他的线程完成某一个动作(如在工作线程中通过Handler对象发送一个Message对象,让UI线程进行UI的更新,然后UI线程就会在MessageQueue中得到这个Message对象(取出Message对象是由其相关联的Looper对象完成的),并作出相应的响应)。

子线程之间的通信方式:
主线程和子线程之间的通信可以通过主线程中的handler把子线程中的message发给主线程中的looper,或者主线程中的handler通过post向looper中发送一个runnable。但looper默认存在于main线程中,子线程中没有Looper,该怎么办呢?其实原理很简单,把looper绑定到子线程中,并且创建一个handler。在另一个线程中通过这个handler发送消息,就可以实现子线程之间的通信了。

子线程创建handler的两种方式:
方式一:给子线程创建Looper对象:
new Thread(new Runnable() {
public void run() {
Looper.prepare(); // 给这个Thread创建Looper对象,一个Thead只有一个Looper对象
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg){
Toast.makeText(getApplicationContext(), “handleMessage”, Toast.LENGTH_LONG).show();
}
};
handler.sendEmptyMessage(1);
Looper.loop(); // 不断遍历MessageQueue中是否有消息
};
}).start();

方式二:获取主线程的looper,或者说是UI线程的looper:
new Thread(new Runnable() {
public void run() {
Handler handler = new Handler( Looper.getMainLooper()){ // 区别在这!!!
@Override
public void handleMessage(Message msg) {
Toast.makeText(getApplicationContext(), “handleMessage”, Toast.LENGTH_LONG).show();
}
};
handler.sendEmptyMessage(1);
};
}).start();

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值