一、基础概念
1 线程与进程
进程
当一个程序第一次启动的时候,Android会启动一个Linux进程和一个主线程。默认的情况下,所有该程序的组件都将在该进程和线程中运行。 同时,Android会为每个应用程序分配一个单独的Linux用户。
Android会尽量保留一个正在运行进程,只在内存资源出现不足时,Android会尝试停止一些进程从而释放足够的资源给其他新的进程使用, 也能保证用户正在访问的当前进程有足够的资源去及时地响应用户的事件。
可以将一些组件运行在其他进程中,并且可以为任意的进程添加线程。
组件运行在哪个进程中是在AndroidManifest.xml文件里设置的,用process
属性来指定该组件运行在哪个进程中。设置这个属性,使每个组件均在各自的进程中运行,或者使一些组件共享一个进程,而其他组件则不共享。 此外,还可以设置 android:process,使不同应用的组件在相同的进程中运行,但前提是这些应用共享相同的 Linux 用户 ID 并使用相同的证书进行签署
线程
线程是系统分配处理器时间资源的基本单元,也是系统调用的基本单位
线程与进程的区别和联系
- 进程是一个动态的过程,是一个活动的实体,是正在执行的程序。一个应用程序的运行就可以被看做是一个进程,而线程是运行中的实际的任务执行者。可以说,进程中包含了多个可以同时运行的线程,线程是进程的一部分。
- 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程。
- 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,那么进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。
- 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。也就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。
- 与进程的控制表PCB相似,线程也有自己的控制表TCB,但是TCB中所保存的线程状态比PCB表中少多了。
- 进程是操作系统资源分配的基本单位,拥有一个完整的虚拟空间地址,而线程是任务调度和执行的基本单位。
简单来说:
一个程序包含进程,进程又包含线程,线程是进程的一个组成部分,进程是操作系统分配资源的基本单位,线程是不会分配资源的,一个进程可以包含多个线程,然后这些线程共享进程的资源。
2 并行与并发
并行
真正意义上的同时进行多种事情,这种只可以在多核CPU的基础下完成。即多个处理器或者多核处理器同时执行多个不同的任务。
并发
从宏观方面来说,并发就是同时进行多种任务,实际上,这几种任务,并不是同时进行的,而是交替进行的,而由于CPU的运算速度非常的快,会造成我们的一种错觉,就是在同一时间内进行了多种事情。即一个处理器处理多个任务。
3 线程状态
- wait()
使一个线程处于等待状态,并且释放所有持有对象的lock锁,直到notify()/notifyAll()被唤醒后放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable) - sleep()
使一个线程处于睡眠状态,是一个静态方法,调用此方法要捕捉Interrupted异常,醒来后进入runnable状态,等待JVM调度 - notify()
使一个等待状态的线程唤醒,注意并不能确切唤醒等待状态线程,是由JVM决定且不按优先级 - allNotify()
使所有等待状态的线程唤醒,注意并不是给所有线程上锁,而是让它们竞争 - join()
使一个线程中断,IO完成会回到Runnable状态,等待JVM的调度 - synchronized()
使Running状态的线程加同步锁使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)
注意:当线程在runnable状态时是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable
4 原子性、可见性、有序性
原子性
一个操作或者一系列操作,要么全部执行要么全部不执行。数据库中的“事物”就是个典型的原子操作。
可见性
当一个线程修改了共享属性的值,其它线程能立刻看到共享属性值的更改。
比如JMM分为主存和工作内存,共享属性的修改过程是在主存中读取并复制到工作内存中,在工作内存中修改完成之后,再刷新主存中的值。若线程A在工作内存中修改完成但还没来得及刷新主存中的值,这时线程B访问该属性的值仍是旧值。这样可见性就没法保证。
有序性
程序执行的顺序按照代码的先后顺序执行。
为了提高性能,编译器和处理器都会对代码进行重新排序,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。(因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行)
指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确
二、基本使用
1 哪些大的地方是执行在主线程的
- Activity/Fragment所有生命周期回调都是执行在主线程的
- Service默认是执行在主线程的
- BroadcastReceiver的onReceiver回调是执行在主线程的
- 没有使用子线程Looper的Handler的handleMessage、post(Runnable)实质是在主线程的
- AsyncTask的回调中除了doInBackground,其他都是执行在主线程的
- View的post(Runnable) 和 postDelayed(Runnable, long是执行在主线程的
- Activity.runOnUiThread(Runnable)
2 使用子线程的方式
- 继承Thread类,或实现Runnable接口
两者的联系:
1、Thread类实现了Runnable接口
2、都需要重写里面run()方法
两者的区别:
1、实现Runnable的类更具有健壮性,避免了单继承的局限
2、Runnable更容易实现资源共享,能多个线程同时处理一个资源 - 使用AsyncTask
- 使用HandlerThread
- 使用IntentService
- 使用Loader
注意:
1)使用工作线程(后台线程)时可能会遇到另一个问题,即:运行时配置变更(例如,用户更改了屏幕方向)导致 Activity 意外重启,这可能会销毁工作线程。
2)使用Thread和HandlerThread时,为了使效果更好,建议设置Thread的优先级偏低一点:Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
因为如果没有做任何优先级设置的话,新建的Thread默认与UI Thread是具有同样优先级的,同样优先级的Thread,CPU调度上还是可能会阻塞掉UI Thread,从而导致ANR。
3 线程终止
使用标志位退出
不断监听标志位的值,可以用volatile修饰该标志位(volatile保证了只有一个线程在操作该标志位)。使用interrupt()方法(注意,不是interrupted)
1)阻塞情况:
线程处于阻塞状态,如使用了sleep,同步锁的wait,socket的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,系统会抛出一个InterruptedException异常,代码中通过捕获异常,然后break跳出循环状态,使线程正常结束。通常很多人认为只要调用interrupt方法线程就会结束,实际上是错的,一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正常结束run方法
2)正常情况:
线程未进入阻塞状态,使用isInterrupted()判断线程的中断标志来退出循环,当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理
3)一般情况:
在正常状态下使用isInterrupted()作为标志位来退出,在阻塞状态下通过捕获异常来退出。因此使用interrupt()来退出线程的最好的方式应该是两种情况都要考虑使用stop()方法
会出现问题,不建议使用。
thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。
三、线程安全
在并发的情况下,代码经过多线程使用,线程的调度顺序不影响任何结果。线程不安全就意味着线程的调度顺序会影响最终结果,比如某段代码不加事务去并发访问。
四、线程同步
通过人为控制和调度,保证共享资源的多线程访问成为线程安全,以保证结果的准确。
1 synchronized
分为方法同步、块同步两种情况。
方法同步
synchronized修饰方法时可以是静态方法、非静态方法,但不能是抽象方法、接口方法。
线程在执行同步方法时是具有排它性的。当任意一个线程进入到一个对象的任意一个同步方法时,这个对象的所有同步方法都被锁定了,在此期间,其他任何线程都不能访问这个对象的任意一个同步方法,直到这个线程执行完它所调用的同步方法并从中退出,从而导致它释放了该对象的同步锁之后。在一个对象被某个线程锁定之后,其他线程是可以访问这个对象的所有非同步方法的。
块同步
通过锁定一个指定的对象,来对代码块进行同步。
同步方法和同步块之间的相互制约只限于同一个对象之间,静态同步方法只受它所属类的其它静态同步方法的制约,而跟这个类的实例没有关系。如果一个对象既有同步方法,又有同步块,那么当其中任意一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其他线程无法在此时访问这个对象的同步方法,也不能执行同步块。
以下1~4参考:https://www.jianshu.com/p/162cf544b637
1)同步非静态方法
每一个对象都有一个内部锁,当使用synchronized关键字声明某个方法时,该方法将受到对象锁的保护,这样一次就只能有一个线程可以进入该方法并获得该对象的锁,其他线程要想调用该方法,只能排队等待。当获得锁的线程执行完该方法并释放对象锁后,别的线程才可拿到锁进入该方法。
2)同一个对象内多个同步方法
当一个线程访问对象的某个synchronized同步方法时,其他线程对该对象中所有其它synchronized同步方法的访问将被阻塞。
当一个线程访问对象的某个synchronized同步方法时,另一个线程仍然可以访问该对象中的非synchronized同步方法。
3)同步代码块
synchronized (obj){}同步代码块和用synchronized声明方法的作用基本一致,都是对synchronized作用范围内的代码进行加锁保护,其区别在于synchronized同步代码块使用更加灵活、轻巧,synchronized (obj){}括号内的对象参数即为该代码块持有锁的对象。
4)同步静态方法
从持有锁的对象的不同我们可以将synchronized同步代码的方式分为两大派系:
synchronized声明非静态方法、同步代码块的synchronized (this){}、synchronized (非this对象){}:
这三者持有锁的对象为实例对象(类的实例对象可以有很多个),线程想要执行该synchronized作用范围内的同步代码,需获得对象锁。synchronized声明静态方法、同步代码块的synchronized (类.class){}:
这两者持有锁的对象为Class对象(每个类只有一个Class对象,而Class对象是Java类编译后生成的.class文件,它包含了与类有关的信息),线程想要执行该synchronized作用范围内的同步代码,需获得类锁。
若synchronized同步方法(代码块)持有锁的对象不同,则多线程执行相应的同步代码时互不干扰;若相同,则获得该对象锁的线程先执行同步代码,其他访问同步代码的线程会被阻塞并等待锁的释放。
2 volatile
参考:https://www.cnblogs.com/dolphin0520/p/3920373.html
一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
用volatile修饰之后:
- 使用volatile关键字会强制将修改的值立即写入主存;
- 使用volatile关键字后,当线程2进行修改xxx时,会导致线程1的工作内存中缓存变量xxx的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
- 由于线程1的工作内存中缓存变量xxx的缓存行无效,所以线程1再次读取变量xxx的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
- volatile没办法保证对变量的操作的原子性。
(synchronized、ReentrantLock、AtomicInteger可以保证原子性) - volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
原理和实现机制
《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
3 重入锁(ReentrantLock)
ReentrantLock重入锁,是实现Lock接口的一个类,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。
支持重入性,需要解决两个问题:
- 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
- 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
公平锁与非公平锁
ReentrantLock支持两种锁:公平锁、非公平锁。公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。
公平锁是按照加锁顺序来的,非公平锁是不按顺序的,也就是说先执行lock方法的锁不一定先获得锁。
ReentrantLock的构造方法无参时是构造非公平锁;还提供了另外一种方式,可传入一个boolean值,true时为公平锁,false时为非公平锁。
- 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序;而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。
- 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。
什么时候应该使用 ReentrantLock ?
在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock “性能会更好”。
ReentrantLock 常用方法
- lock():获得锁
- unlock():释放锁
- getHoldCount():查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数
- getQueueLength():返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9
- getWaitQueueLength(Condition condition):返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition对象,并且此时这10个线程都执行了condition对象的await方法,那么此时执行此方法返回10
- hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了condition.await方法
- hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
- hasQueuedThreads():是否有线程等待此锁
- isFair():该锁是否公平锁
- isHeldByCurrentThread():当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true
- isLock():此锁是否有任意线程占用
- lockInterruptibly():如果当前线程未被中断,获取锁
- tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
- tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁
4 阻塞队列(BlockingQueue)
包括:LinkedBlockingQueue、ArrayBlockingQueue。
ArrayBlockingQueue 是一个用数组实现的有界阻塞队列。
区别
- 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReentrantLock锁;而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock。
- 队列大小有所不同,ArrayBlockingQueue是有界的,初始化必须指定大小;而LinkedBlockingQueue可以是有界的,也可以是无界的(Integer.MAX_VALUE)。
- ArrayBlockingQueue采用的是数组当做存储容器;而LinkedBlockingQueue采用的则是链表方式,与ArrayList和LinkedList类似。
LinkedBlockingQueue是一个线程安全的阻塞队列,实现了先进先出等特性。
可以指定容量,也可以不指定,不指定的话默认最大是Integer.MAX_VALUE。
LinkedBlockingQueue 类的常用方法
其中主要用到put()和take()方法:
- put(anObject):将一个对象放到队列尾部,在队列满的时候会阻塞直到有队列成员被消费。
- take():获取对头(head)元素,在队列为空的时候会阻塞,直到有队列成员被放进来。
- add(anObject):把anObject添加到BlockingQueue里,添加成功返回true,如果BlockingQueue空间已满则抛出异常。
- offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false。
- poll(time):获取并移除此队列的头,若不能立即取出,则可以等time参数规定的时间,取不到时返回null。
- clear():从队列彻底移除所有元素。
- remove():直接删除队头的元素。
- peek():直接取出队头的元素,并不删除。
5 原子变量(AtomicInteger)
AtomicInteger是JAVA原子操作的Interger类,线程安全,使用原子锁。
AtomicInteger通过一种线程安全的加减操作接口,也就是说当有多个线程操作同一个变量时,使用AtomicInteger不会导致变量出现问题,而且比使用 synchronized效率高。
常用方法:get()、set()、getAndIncrement()、getAndDecrement()等。
包名 java.util.concurrent.atomic,该包名下包含其它同步数值类 AtomicBoolean、AtomicLong等。
注意:创建AtomicInteger对象时是作为成员变量使用的,不要在局部区域使用此对象。
6 使用线程池进行管理及优化
五、线程死锁
多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些线程都将无法向前推进。
产生原因
系统资源的竞争
通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。线程推进顺序不当
线程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,线程P1、P2分别保持了资源R1、R2,而P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞。死锁产生的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生:
1) 互斥条件:
线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。
2)不剥夺条件:
线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
3)请求和保持条件:
线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放。
4)循环等待条件:
存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的线程集合{Pl, P2, …, Pn},其中Pi等待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有。
避免死锁
1 设置加锁顺序
线程按照一定的顺序加锁。
假如一个线程需要锁,那么他必须按照一定得顺序获得锁。
例如加锁顺序是A->B->C,现在想要线程C想要获取锁,那么他必须等到线程A和线程B获取锁之后才能轮到他获取。(排队执行,获取锁)
缺点:
按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要事先知道所有可能会用到的锁(并对这些锁做适当的排序),但总有些时候是无法预知的
2 设置加锁时限
在获取锁的时候加一个获取锁的时限,超过时限不再获取锁,放弃操作(对锁的请求)。
若一个线程没有在给定时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段时间再重试。在这段等待时间中,其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续执行别的逻辑(加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。
如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是几十几百个线程,情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。
(超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争、等待,再次死锁)
3 死锁检测
主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。
检测出死锁后,应该怎么做
释放所有锁,回退,并且等待一段随机的时间后重试。
这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁。不能从根本上减轻竞争。给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。
如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。
六、线程间通信
1 AsyncTask
AsyncTask 允许对用户界面执行异步操作。 它会先阻塞工作线程中的操作,然后在 UI 线程中发布结果,而无需亲自处理线程或处理程序。
本质上是对ThreadPool和Handler的一个封装。
默认是串行的执行任务,可以调用 executeOnExecutors 方法并行执行任务。
步骤:
1)必须创建 AsyncTask 的子类并实现 doInBackground() 回调方法,该方法将在后台线程池中运行。
2)要更新 UI,应该实现 onPostExecute() 以传递 doInBackground() 返回的结果并在 UI 线程中运行,以便您安全地更新 UI。
3)在 UI 线程调用 execute() 来运行任务。
AsyncTask 的工作方法简述:
- 可以使用泛型指定参数类型、进度值和任务最终值
- 方法 doInBackground() 会在工作线程上自动执行
- onPreExecute()、onPostExecute() 和 onProgressUpdate() 均在 UI 线程中调用
- doInBackground() 返回的值将发送到 onPostExecute()
- 可以随时在 doInBackground() 中调用publishProgress(),以在 UI 线程中执行 onProgressUpdate()
- 可以随时取消任何线程中的任务
2 Handler
Handler + MessageQueue + Looper
1)Message:消息体,用于装载需要发送的对象。
2)MessageQueue:
- 用于存放Message或Runnable对象的消息队列。
由对应的Looper对象创建和管理。每个线程中只会有一个MessageQueue对象。 - 本质上是一个单链表,采用FIFO方式(先进先出)管理,用enqueueMessage()方法将消息插入一条队列,next()方法是一个无限循环的方法,如果有消息,则取出,如果没有,则阻塞。
3)Handler:
- 作用:在子线程中发送Message或者Runnable对象到MessageQueue中;在UI线程中接收、处理从MessageQueue分发出来的Message或Runnable对象。
- 发送消息一般使用Handler的sendMessage()方法,而发出去的消息经过处理后最终会传递到Handler的handleMessage()方法中。
4)Looper:
- 每个线程中MessageQueue的管家,循环不断地管理MessageQueue接收和分发Message或Runnable的工作。
- 调用Looper的loop()方法后,会进入到一个无限循环中,每当发现MessageQueue中存在一条消息,就将它取出,并调用Handler的handleMessage()方法。
- 每个线程中只有一个Looper对象。
- 线程是默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。UI线程(ActivityThread)被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。
一个Handler对应一个Looper对象(但一个Looper可以有多个Handler),一个Looper对应一个MessageQueue对象,使用Handler生成Message,而一个Handler可以生成多个Message(Handler和Message时一对多的关系)。
Handler和Looper对象是属于线程内部的数据,不过也提供与外部线程的访问接口,Handler就是公开给外部线程的接口,用于线程间的通信。Looper是由系统支持的用于创建和管理MessageQueue的依附于一个线程的循环处理对象,而Handler是用于操作线程内部的消息队列的,所以Handler也必须依附一个线程,且只能是一个线程。
异步消息处理机制
当应用程序启动时,系统会自动为UI线程创建一个MessageQueue和Looper。首先需要在主线程中创建一个Handler并重写handleMessage()方法,当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,并找到与Message对应的Handler对象,调用Handler的handleMessage()方法。
ThreadLocal
ThreadLocal并不是线程,它的作用是可以在每个线程中存储数据。
Handler创建的时候会采用当前线程的Looper来构造消息循环系统,那么Handler内部如何获取到当前线程的Looper呢?这就要使用ThreadLocal了,ThreadLocal可以在不同的线程之中互不干扰地存储并提供数据,通过ThreadLocal可以轻松获取每个线程的Looper。
ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其它线程来说无法获取到数据。
一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。
比如:对于Handler来说,它需要获取当前线程的Looper,很显然Looper的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松实现Looper在线程中的存取,如果不采用ThreadLocal,那么系统就必须提供一个全局的哈希表供Handler查找指定线程的Looper,这样一来就必须提供一个类似于LooperManager的类了,但是系统并没有这么做而是选择了ThreadLocal,这就是ThreadLocal的好处。
Android的源码中的使用场景:Looper、ActivityThread、AMS中都用到了ThreadLocal。
3 IntentService
IntentService是一个抽象类,封装了HandlerThread和Handler,负责处理耗时的任务。任务执行完毕后会自行停止。在onCreate方法中开启了一个HandlerThread线程,之后通过HandlerThread的Looper初始化了一个Handler,负责处理耗时操作。通过startService方法启动,在Handler中调用抽象方法onHandleIntent(),该方法执行完成后自动调用stopSelf()方法停止。
须重写 onHandleIntent方法。
优点:
不需要自己去创建线程;
不需要考虑在什么时候关闭该Service。
4 HandlerThread
本质上是继承了Thread的线程类。
通过创建HandlerThread获取Looper对象,传递给Handler对象,执行异步任务。创建HandlerThread后必须先调用start()方法,才能调用getLooper()获取Looper对象。
在HandlerThread中通过Looper.prepare()方法来创建消息队列,并通过Looper.loop()来开启消息循环。
HandlerThread封装了Looper对象,使我们不用关心Looper的开启和释放的细节问题。如果不用HandlerThread的话,需要手动去调用Looper.prepare()和Looper.loop()这些方法。
5 Loader
Google Doc: https://developer.android.google.cn/guide/components/loaders.html
七、线程池
管理线程,减少内存消耗,核心:回收循环利用
优势:
- 重用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销;
- 线程池旨在线程的复用,就避免了创建线程和销毁线程所带来的时间消耗,减少线程频繁调度的开销,从而节约系统资源,提高系统吞吐量;
- 能有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象;
- 能够对线程进行简单的管理,并提供定时执行以及指定时间间隔循环执行等功能;
线程池主要用来解决线程生命周期开销问题和资源不足问题:
- 通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样就可以立即为请求服务,使应用程序响应更快。
- 通过适当的调整线程池中的线程数目可以防止出现资源不足的情况。
组成部分
一个比较简单的线程池至少应包含线程池管理器、工作线程、任务队列、任务接口等部分。
- 线程池管理器:创建、销毁并管理线程池,将工作线程放到线程池中。
- 工作线程:一个可以循环执行任务的线程,在没有任务时进行等待。
- 任务队列:提供一种缓冲机制,将没有处理的任务放在任务队列中。
- 任务接口:每个任务必须实现的接口,主要用来规定任务的入口、任务执行完后的收尾工作、任务的执行状态等,工作线程通过该接口调度任务的执行。
创建线程池:
new ThreadPoolExecutor,通过不同参数创建不同类型的线程池
执行步骤
任务进来时,首先判断核心线程是否处于空闲状态。如果不是,核心线程就先执行任务,如果核心线程已满,则判断任务队列是否有地方存放该任务,如果有,就将任务保存在任务队列中,等待执行,如果满了,再判断最大可容纳的线程数,如果没有超出这个数量,就开创非核心线程执行任务,如果超出了,就调用Handler实现拒绝策略。
Handler拒绝策略:
- AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满。
- DisCardPolicy:不执行新任务,也不抛出异常。
- DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行。
- CallerRunsPolicy:直接调用execute来执行当前任务。
常见线程池
CachedThreadPool 可缓存的线程池
该线程池中没有核心线程,非核心线程的数量为Integer.MAX_VALUE即无限大。
当有需要时创建线程来执行任务,没有需要时回收线程。
适用于耗时少、任务量大的情况。
创建带有缓存的线程池,在执行新的任务时,当线程池中有之前创建的可用线程就重用之前的线程,否则就新建一条线程。如果线程池中的线程在60秒未被使用,就会将它从线程池中移除。ScheduleThreadPool 周期性执行任务的线程池
按照某种特定的计划执行线程中的任务。
有核心线程,也有非核心线程,非核心线程的大小为无限大。
适用于执行周期性的任务。
创建定时和周期性执行的线程池。SingleThreadPool 单线程的线程池
只有一条线程来执行任务。
适用于有顺序的任务的应用场景。
创建一个单线程的线程池,即这个线程池永远只有一个线程在运行,这样能保证所有任务按指定顺序来执行。如果这个线程异常结束,那么会有一个新的线程来替代它。FixedThreadPool 定长的线程池
有核心线程,核心线程数即为最大线程数量,没有非核心线程。
创建固定大小的线程池,这样可以控制线程最大并发数,超出的线程会在队列中等待。如果线程池中的某个线程由于异常而结束,线程池则会再补充一条新线程。