Android 中线程的使用
线程
Android官网文档->https://developer.android.com/guide/components/processes-and-threads.html?hl=zh-cn#Threads
应用启动时,系统会为应用创建一个线程,称为主线程;它负责UI的绘制以及UI的事件响应交互,也称为UI线程;
系统不会为每个组件实例创建单独的线程,同一进程中的所有组件都在主线程实例化,并且每个组件的资源调用都由主线程分配,因此响应系统回调都在主线程进行。
因为主线程要处理UI的绘制及事件的交互,所以主线程中不能进行耗时的操作(网络访问,数据库操作),一旦主线程进行耗时操作就会出现阻塞,UI事件就没办法响应了,就会出现ANR,这是非常不友好的。
Android UI是非线程安全的,所以关于UI的操作只能在UI线程操作,所以Android单线程模式必须遵守两条规则
- 不能阻塞UI线程
- UI操作要在UI线程,不要在 UI 线程之外访问 Android UI 工具包
工作线程
为了保证应用的顺畅,所有耗时的操作都在工作线程中进行。
遵循上述的两条规则,不能再UI线程之外的线程访问UI,但是网络访问结果是在工作线程,要将结果填充到UI中怎么办呢,Android提供了几种方法在工作线程中访问UI
- Activity.runOnUiThread(Runnable)
- View.post(Runnable)
- View.postDelayed(Runnable, long)
- Handler
还有一种方式 AsyncTask;
线程相关API
- Runnable
- Thread
- Callable
- Future<?>
- RunnableFuture
- FutureTask
- Executor
- ExecutorService
- ThreadPoolExecutor
- Executors
创建线程
有两种方式创建一个线程。
- 继承 Thread类
实现 Runnable接口
继承Thread类 重写 run方法,在调用start()后JVM会自动调用run()方法
1 | /** |
实现 Runnable接口
1 |
|
启动线程
继承Thread方式
1 | MyThread thread0 = new MyThread(); |
实现 Runnable方式
1 | MyRunnable myRunnable = new MyRunnable(); |
Runnable和Thread两种方式的区别和联系
其实这块主要是围绕着接口和抽象类的区别以及一些设计原则而言的。
- Java是单继承的,你继承了Thread类再也无法继承别的类。
- Java虽然是单继承但是可以实现多接口的,即使你实现Runnable,你也可以实现别的接口。
- 在面向对象编程中,继承一个类就意味着要使用或者改善某些功能,如果不准备改善Thread提供的封装好的功能,使用Runnable更好。
- Runnable接口表示一个任务,可以被任意一个普通线程,线程池或者其他方式执行。逻辑上的分离,Runnable比Thread更好。
- 将任务分离为Runnable,就可以重用或者通过其他方式执行它,Thread一旦完成就无法重新启动了。
- 线程池的接口接收的参数为Runnable
- 用Runnable能够代表一个线程就不必继承Thread,那样就额外的继承了Thread的全部方法
- 继承Thread,你就必须为每个线程都创建一个实例,然后为每个实例分配内存。
- 写一个接口而不是实现,会让程序更容易扩展。
Thread类也是实现了Runnable接口
参考资料
- https://www.linkedin.com/pulse/20140917120728-90925576-difference-between-implements-runnable-and-extends-thread-in-java
- https://my.oschina.net/leejun2005/blog/483999
- 这个重点看评论->http://mars914.iteye.com/blog/1508429
资源共享及同步问题
关于多线程资源共享,多线程并发操作有随机性,不能保证每个线程都顺序的去访问某个资源,在多个线程同时去访问一个资源的时候要进行资源的同步.
经典的卖票例子
资源共享
资源共享,多个线程并发执行访问同一个资源,才是共享的资源。
票
1 | /** |
卖票程序
1 |
|
卖票
1 | //票资源 |
卖票结果
多线程并发执行的结果就是 多个窗口同时售票,同一时间销售掉了同一个票,尴尬了,一个票卖了多次
1 | 08-22 14:23:41.066 E/窗口零: 销售第5张,剩余4张 |
资源同步
Java 同步块(synchronized block)用来标记方法或者代码块是同步的
java中每个对象都对应于一个称为“互斥锁”的标志,这个标志用来保证在任何时刻,只能有一个线程访问该对象。
如果系统中的资源当前没有被使用,线程可以得到“互斥锁”,即线程可以得到资源的使用权。
当线程执行完毕后,他放弃“互斥锁”,如果一个线程获得“互斥锁”时,其余的线程就必须等待当前线程结束并放弃“互斥锁”。
在java中,提供了关键字synchronized来实现对象的“互斥锁”关系。
当某个对象或方法用关键字synchronized修饰时,表明该对象或方法在任何一个时刻只能有一个线程访问。
如果synchronized用在类的声明中,表明该类中的所有方法都是synchronized的。
在这个例子中,我们只需要将“票”这个资源同步即可
多个线程都是访问的这一个实例,所以同步这个实例方法,就可以了;
1 | /** |
或者
1 | /** |
学习资料
- http://ifeve.com/java-concurrency-thread-directory/
- http://ifeve.com/synchronized-blocks/
- http://student-lp.iteye.com/blog/2083170
- http://www.cnblogs.com/dennisit/archive/2013/02/24/2925288.html
- http://lanvis.blog.163.com/blog/static/26982162009798422547/
线程状态
- 新生状态(new)
当一个线程的实例被创建即使用new关键字后台Thread类或者其子类创建一个线程对象后,此时该线程就处于新生状态,处于新生状态的线程有自己的内存空间,但该线程并没有运行,此时线程还不是活着的(not alive)
- 就绪状态(Runnable)
通过调用线程实例的start()方法来启动线程使线程进入就绪状态(runnable);处于就绪状态的线程已经具备了运行条件,但还没被分配到CPU就是不一定会被立即执行,此时处于线程就绪队列,等待线程为期分配CPU,等待状态不是执行状态;此时线程是活着的(alive);
- 运行状态(Running)
一旦获取CPU(被JVM选中),线程就进入运行(running)状态,线程run()方法才开始被执行;在运行状态的线程执行自己的run()方法中的操作,知道调用其他的方法而终止、或者等待某种资源而阻塞、或者完成任务而死亡;如果在给定的时间片内没有执行结束,就会被系统给换下来回到线程的就绪状态;此时线程是活着的(alive);
- 阻塞状态(Blocked)
通过调用join(),sleep(),wait()或者资源被占用使线程处于阻塞(blocked)状态;处于Blocked状态的线程仍然是活着的(alive);
- 死亡状态(Dead)
当一个线程的run()方法运行完毕或被中断或被异常退出,该线程到达死亡(dead)状态。此时可能仍然存在一个该Thread的实例对象,但该Thread已经不可能在被作为一个可被独立执行的线程对待了,线程的独立的call stack已经被dissolved。一旦某一线程进入Dead状态,他就再也不能进入一个独立线程的生命周期了。对于一个处于Dead状态的线程调用start()方法,会出现一个运行期(runtime exception)的异常;处于Dead状态的线程不是活着的(not alive)。
学习资料
- http://www.cnblogs.com/DreamSea/archive/2012/01/11/JavaThread.html
- http://www.cnblogs.com/whoislcj/p/5603277.html
- http://student-lp.iteye.com/blog/2083170
线程通信
Java中常规的通信方式这里我就不说了,看一下Android的消息机制
Java常规的通信方式传送门->http://ifeve.com/thread-signaling
Android中的消息机制可以用于线程间通信也可用于在各个组件间通信,这里只总结一下怎么在线程间使用
消息机制中重要的API
- Message 线程间通信就是在传递消息,Message就是消息的载体。常用的有四个字段:arg1,arg2,what,obj。obj可以携带Object对象,其余三个可以携带整形数据
- MessageQueue 消息队列,它主要用于存放所有通过Handler发送的消息(也就是一个个Message),这部分的消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。
- Looper 每个线程通过Handler发送的消息都保存在,MessageQueue中,Looper通过调用loop()的方法,就会进入到一个无限循环当中,然后每当发现MessageQueue中存在一条消息,就会将它取出,并传递到Handler的handleMessage()方法中。
- Handler 发送消息,处理消息
- Thread 线程 每个线程中只会有一个Looper对象。
运行机制
在哪个Thread中创建Handler,默认情况下Handler就会获取哪个线程中的Looper(前提是Looper创建好了);handler发送消息就是将消息发送到了自己持有的这个Looper对象里;
Looper内有一个MessageQueue,消息就存放在队列里,一旦Looper的loop()方法被调用就会开启无限循环模式,一直循环遍历这个队列,从中取Handler发送的消息,没有消息就阻塞;一旦有消息就唤醒线程取出来;
从MessageQueue中取出的消息,会调用本身target持有的handler实例来处理这个消息;
综上所述,线程间通信handler就可以实现;
主线程给工作线程发消息
想要在主线程给工作线程发消息,我们就得持有在工作线程中创建的handler;
而创建handler之前必须先初始化一下Looper对象;
handler创建完之后就开启Looper的无限循环来等待消息
创建一个线程并创建一个handler
1 | Handler handlerA = null; |
使用handlerA发送消息
1 | //创建一个消息 |
看一个log 处理线程是ThreadA,消息来源是Main线程1
08-23 16:26:02.609 E/handleMessage: ThreadA;src->main
工作线程发给主线程
与上面的同理,想要给主线程发送消息,拿到主线程的handler就可以了;
因为点击事件是在UI线程中响应的,所以想让工作线程给主线程发送一个消息就麻烦一点,我这里为了测试做了个中转,先给B线程发送一个信号,B接到这个信号就给主线程发消息
1 |
|
在主线程创建的handler
1 | /** |
主线程的handler创建时没有提前创建Looper也没有调用Looper的loop()方法,是因为程序在启动的时候已经为主线程创建好了Looper,并且调用了loop(),一直在等待消息
工作线程给工作线程发消息
跟上面两个一样,想给哪个线程发消息就要先拿到哪个线程的handler;我这里就不贴代码了;
学习资料
- http://www.jianshu.com/p/02962454adf7
- http://www.jianshu.com/p/7657f541c461
- http://www.cnblogs.com/younghao/p/5116819.html
- http://www.voidcn.com/article/p-pmejydob-bbs.html
线程池
为啥使用线程池
- 减少频繁创建销毁线程带来的开销
- 复用创建好的线程
- 对线程进行简单的管理
ExecutorService是一个接口,定义了线程池的方法。
1 | public interface ExecutorService extends Executor { |
ThreadPoolExecutor具体实现了ExecutorService接口,提供了一系列的参数来配置线程池,熟悉ThreadPoolExecutor可自定义线程池。
1 | public ThreadPoolExecutor(int corePoolSize, |
- corePoolSize 核心线程池数量
- maximumPoolSize 最大线程数量
- keepAliveTime 非核心线程闲置的超时时长,超过这个时长,非核心线程就会被回收,当allowCoreThreadTimeOut为true时,keepAliveTime同样作用于核心线程。
- unit 时间单位,常用的为 TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟),详情-》https://docs.oracle.com/javase/6/docs/api/java/util/concurrent/TimeUnit.html
- workQueue 线程池中的任务队列,通过execute方法提交的Runnable对象会存储在这个参数中
- threadFactory 线程工厂,为线程池提供创建线程的功能,是个接口,提供Thread newThread(Runnable r)方法;通常用默认的
Executors.defaultThreadFactory()即可 - handler 拒绝策略,当线程池无法执行新任务时,可能由于线程队列已满或无法成功执行任务,这时候 ThreadPoolExecutor会调用handler的 rejectedExecution的方法,默认会抛出RejectedExecutionException
Android通过Executors为我们提供了几种线程池
| 方法 | 说明 |
|---|---|
| Executors.newCachedThreadPool() | 缓存线程池,线程数量不定,最大线程数为Integer.MAX_VALUE(相当于任意大),有新任务时会检查是否有空闲线程,没有则会创建线程,空闲线程超过60s会被回收,任何任务都会被立即执行,适合大量的耗时较少任务 |
| Executors.newFixedThreadPool(int nThreads) | 固定型线程池,线程数量固定,只有核心线程并且无超时机制,当所有线程都执行任务时,新任务进入队列等待。能够快速响应请求 |
| Executors.newScheduledThreadPool(int corePoolSize) | 调度型线程池,核心数量固定,非核心数量无限制,非核心线程一旦空闲立马回收。会根据Scheduled(任务列表)进行延迟执行,或者是进行周期性的执行.适用于一些周期性的工作 |
| EExecutors.newSingleThreadExecutor() | 单例线程池,只有一个核心线程,所有任务都在这个线程中串行执行,不需要处理线程同步问题,在任意的时间段内,线程池中只有一个线程在工作… |
在ExecutorService的方法中可以看到线程池除了可执行Runnable接口还可以执行Callable<V> 接口,并且可以通过Future<V>来感知线程状态和结果。
因为Runnable的返回值为void 无法获取执行完毕后的结果 ,所以才有了Callable<V>,可以返回一个结果1
2
3
4
5
6
7
8
9public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Future<V> 定义了几种方法来感知线程状态和获取结果 ,可以理解为管理线程的。Future提供了三种功能:
- 判断任务是否完成;
- 能够中断任务;
- 能够获取任务执行结果。
1 | public interface Future<V> { |
因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。
可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。
所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。1
public class FutureTask<V> implements RunnableFuture<V> {}
1 | public interface RunnableFuture<V> extends Runnable, Future<V> { |
ScheduledExecutorService 调度线程池 ,扩展了ExecutorService,增加了几个方法用来执行周期性或者定时的任务
1 | public interface ScheduledExecutorService extends ExecutorService { |
关于几种线程池的使用,代码都放在我的GitHub了。
学习资料
- https://developer.android.com/reference/java/util/concurrent/ScheduledExecutorService.html
- https://tom510230.gitbooks.io/android_ka_fa_yi_shu_tan_suo/content/chapter11.html
- http://www.cnblogs.com/whoislcj/p/5607734.html
- http://blog.csdn.net/u010687392/article/details/49850803
- http://www.cnblogs.com/RGogoing/p/4766971.html
- http://www.cnblogs.com/dolphin0520/p/3949310.html
关于本次的Demo https://github.com/sky-mxc/AndroidDemo/tree/master/thread
没有涉及到的地方,欢迎补充。错误的地方,感谢指正