目录
前言
我们知道Android3.0后如果在主线程进行网络请求是会抛出异常的,这是为了避免主线程被耗时操作阻塞从而导致ANR。因此有必要学习一下子线程相关知识。
正文
搞懂什么是线程前,也需要搞懂什么是进程。
什么是进程
进程是操作系统结构的基础。
进程是程序在一个数据集合上运行的过程。
进程是系统进行资源分配的基本单位。
进程可以看着程序的一个实体,也是线程的一个容器。
通常,一个app是存在一个进程,但通过配置,一个app可以存在多个进程。(PS:微信、QQ等都存在多个进程)
什么是线程
线程是操作系统调度的最小单位。可被称为轻量级的进程。
一个进程可以有多个线程,每个线程都拥有各自的计数器,堆栈和局部变量等属性,同时也能够访问共享 的内存变量。
为啥需要多线程
-
使用多线程可以减少程序的响应时间。
-
与进程相比,创建和切换线程开销更小,同时多线程可共享数据,进程需要通过一定方式共享。
-
硬件支持,比如多CPU和多核的设备支持多线程的能力,如果只是单线程,那就容易浪费资源。
-
使用多线程简化程序结构,便于理解和维护?【没太懂,是相对进程来说?】
线程状态
线程在生命周期中存在下面6中状态
1. New: 创建
线程被创建, 还没有调用 start 方法, 在线程运行之前还有一些基础工作要做。
2. Runnable: 可运行
一旦调用start方法, 线程就处于Runnable状态。 一个可运行的线程可能正在运行也可能没有运行, 这取决于操作系统给线程提供运行的时间。
3. Blocked: 阻塞
表示线程被锁阻塞, 它暂时不活动。
4. Waiting: 等待
线程暂时不活动, 并且不运行任何代码, 这消耗最少的资源, 直到线程调度器重新激活它。
5. Timed waiting: 超时等待
和等待状态不同的是, 它是可以在指定的时间自行返回的。
6. Terminated: 终止
表示当前线程已经执行完毕。 导致线程终止有两种情况: 第一种就是run方法执行完毕正常退出; 第二种就是因为一个没有捕获的异常而终止了run方法, 导致线程进入终止状态。
创建线程
创建线程有有三种方式,前面两种比较常用的,应该都会,至于Callable,可以看《》,用法跟Runnable差不多,但会有返回值。
-
继承Thread
-
实现Runnable
-
实现Callable
中断interrupt
当一个线程调用interrupt方法时, 线程的中断标识位将被置位( 中断标识位为true) , 线程会不时地检测这个中断标识位, 以判断线程是否应该被中断。
要想知道线程是否被置位, 可以调用Thread.currentThread() .isInterrupted() 。还可以调用Thread.interrupted() 来对中断标识位进行复位。
但是如果一个线程被阻塞, 就无法检测中断状态。
如果一个线程处于阻塞状态, 线程在检查中断标识位时如果发现中断标识位为true, 则会在阻塞方法调用处抛出InterruptedException异常, 并且在抛出异常前将线程的中断标识位复位, 即重新设置为 false。
需要注意的是被中断的线程不一定会终止, 中断线程是为了引起线程的注意, 被中断的线程可以决定如何去响应中断。 如果是比较重要的线程则不会理会中断, 而大部分情况则是线程会将中断作为一个终止的请求。
如何处理中断
下面介绍两种比较合理的中断处理方法。
方式一
在catch子句中, 调用Thread.currentThread.interrupt() 来设置中断状态(因为抛出异常后中断标识位会复位) , 让外界通过判断Thread.currentThread().isInterrupted() 来决定是否终止线程还是继续下去。
public void run(){ try{ sleep(1000); }catch(InterruptedException e){ Thread.currentThread().interrupted(); } }
方式二
更好的做法就是, 不使用try来捕获这样的异常, 让方法直接抛出, 这样调用者可以捕获这个异常。
public void run(){ sleep(1000); }
安全终止线程
判断中断状态
public void run(){ while(!Thread.currentThread.interrupt()){ //do something } }
新增判断条件
public volatile boolean isRunning = true; public void run(){ //根据isRunning条件退出 while(isRunning){ //do something } } public void cancel(){ isRunning = false; }
同步
如果两个线程存取相同的对象, 并且每一个线程都调用了修改该对象的方法,如果不加锁,就容易出现脏数据。
synchronized关键字自动提供了锁以及相关的条件。
同步方法
如果一个方法用 synchronized 关键字声明, 那么对象的锁将保护整个方法。
public synchronized void fun(){ //do something }
同步代码块
synchronized(object){ //do something }
同步代码块是非常脆弱的,通常不推荐使用。
一般实现同步最好用java.util.concurrent包下提供的类, 比如阻塞队列。 如果同步方法适合你的程序, 那么请尽量使用同步方法, 这样可以减少编写代码的数量, 减少出错的概率。 如果特别需要使用Lock/Condition结构提供的独有特性时, 才使用Lock/Condition。
原子性、 可见性和有序性
原子性
对基本数据类型变量的读取和赋值操作是原子性操作, 即这些操作是不可被中断的, 要么执行完毕, 要么就不执行。
//原子性操作,只是赋值 int x = 3; //不是原子性操作,具有2步操作,先读x,然后赋值给y int y= x; //不是原子性操作,具有3步操作,先读x,再x+1,最后后赋值给x x++;
可见性
可见性, 是指线程之间的可见性, 一个线程修改的状态对另一个线程是可见的。
可以认为一个线程修改的结果, 另一个线程马上就能看到。
当一个共享变量被volatile修饰时, 它会保证修改的值立即被更新到主存, 所以对其他线程是可见的。 当有其他线程需要读取该值时, 其他线程会去主存中读取新值。
而普通的共享变量不能保证可见性, 因为普通共享变量被修改之后, 并不会立即被写入主存, 何时被写入主存也是 不确定的。 当其他线程去读取该值时, 此时主存中可能还是原来的旧值, 这样就无法保证可见性。
有序性
Java内存模型中允许编译器和处理器对指令进行重排序, 虽然重排序过程不会影响到单线程执行的正确 性, 但是会影响到多线程并发执行的正确性。
这时可以通过volatile来保证有序性, 除了volatile, 也可以通过synchronized和Lock来保证有序性。
volatile
有时仅仅为了读写一个或者两个实例域就使用同步的话, 显得开销过大; 而volatile关键字为实例域的 同步访问提供了免锁的机制。
当一个共享变量被volatile修饰之后, 其就具备了两个含义, 一个是线程修改了变量的值时, 变量的新 值对其他线程是立即可见的。
换句话说, 就是不同线程对这个变量进行操作时具有可见性。 另一个含义是禁止使用指令重排序。
volatile不保证原子性 ,但volatile能保证有序性 。
正确使用volatile关键字
synchronized关键字可防止多个线程同时执行一段代码, 那么这就会很影响程序执行效率。 而volatile关 键字在某些情况下的性能要优于synchronized。 但是要注意volatile关键字是无法替代synchronized关键字的, 因为volatile关键字无法保证操作的原子性。
通常来说, 使用volatile必须具备以下两个条件:
-
对变量的写操作不会依赖于当前值
-
该变量没有包含在具有其他变量的不变式中。
第一种是因为volatile不保证原子性,所以不能是自增、 自减等操作。
第二种就是变量不能在其他地方改变
常用场景
-
状态标志
public volatile boolean isRunning = true; public void run(){ //根据isRunning条件退出 while(isRunning){ //do something } }
-
双重检查模式(DCL)
就是单例模式中的双重检查,Singleton 进行2次null判断。
private volatile static Singleton instance = null;
在这里用到了volatile关键字会或多或少地影响性能, 但考虑到程序的正确性, 牺牲这点性能还是值得的。 DCL的优点是资源利用率高, 第一次执行getInstance方法时单例对象才被实例化, 效率高。 其缺点是第一次加载时反应稍慢一些, 在高并发环境下也有一定的缺陷(虽然发生的概率很小) 。
参考文章
1. 《Android进阶之光》