Android多线程介绍

Android2023年7月5日 am8:08发布1年前 (2023)更新 3XCN.COM站长
0 0 0
广告也精彩
目录

前言

记录一下Android线程,子线程等相关知识。

我们知道Android3.0后如果在主线程进行网络请求是会抛出异常的,这是为了避免主线程被耗时操作阻塞从而导致ANR。因此有必要学习一下子线程相关知识。

正文

搞懂什么是线程前,也需要搞懂什么是进程

什么是进程

进程是操作系统结构的基础。

进程是程序在一个数据集合上运行的过程。

进程是系统进行资源分配的基本单位。

进程可以看着程序的一个实体,也是线程的一个容器。

通常,一个app是存在一个进程,但通过配置,一个app可以存在多个进程。(PS:微信、QQ等都存在多个进程)

什么是线程

线程是操作系统调度的最小单位。可被称为轻量级的进程。

一个进程可以有多个线程,每个线程都拥有各自的计数器,堆栈和局部变量等属性,同时也能够访问共享 的内存变量。

为啥需要多线程

  1. 使用多线程可以减少程序的响应时间。

  2. 与进程相比,创建和切换线程开销更小,同时多线程可共享数据,进程需要通过一定方式共享。

  3. 硬件支持,比如多CPU和多核的设备支持多线程的能力,如果只是单线程,那就容易浪费资源。

  4. 使用多线程简化程序结构,便于理解和维护?【没太懂,是相对进程来说?】

线程状态

线程在生命周期中存在下面6中状态

1. New: 创建

线程被创建, 还没有调用 start 方法, 在线程运行之前还有一些基础工作要做。

2. Runnable: 可运行

一旦调用start方法, 线程就处于Runnable状态。 一个可运行的线程可能正在运行也可能没有运行, 这取决于操作系统给线程提供运行的时间。

3. Blocked: 阻塞

表示线程被锁阻塞, 它暂时不活动。

4. Waiting: 等待

线程暂时不活动, 并且不运行任何代码, 这消耗最少的资源, 直到线程调度器重新激活它。

5. Timed waiting: 超时等待

和等待状态不同的是, 它是可以在指定的时间自行返回的。

6. Terminated: 终止

表示当前线程已经执行完毕。 导致线程终止有两种情况: 第一种就是run方法执行完毕正常退出; 第二种就是因为一个没有捕获的异常而终止了run方法, 导致线程进入终止状态。

Android多线程介绍

创建线程

创建线程有有三种方式,前面两种比较常用的,应该都会,至于Callable,可以看《Callable的简单使用》,用法跟Runnable差不多,但会有返回值。

  1. 继承Thread

  1. 实现Runnable

  1. 实现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必须具备以下两个条件:

  1. 对变量的写操作不会依赖于当前值

  2. 该变量没有包含在具有其他变量的不变式中。

第一种是因为volatile不保证原子性,所以不能是自增、 自减等操作。

第二种就是变量不能在其他地方改变

Android多线程介绍

常用场景
  1. 状态标志

public volatile boolean isRunning = true;
public void run(){
    //根据isRunning条件退出
    while(isRunning){
        //do something
    }
}
  1. 双重检查模式(DCL)

就是单例模式中的双重检查,Singleton 进行2次null判断。

private volatile static Singleton instance = null;

在这里用到了volatile关键字会或多或少地影响性能, 但考虑到程序的正确性, 牺牲这点性能还是值得的。 DCL的优点是资源利用率高, 第一次执行getInstance方法时单例对象才被实例化, 效率高。 其缺点是第一次加载时反应稍慢一些, 在高并发环境下也有一定的缺陷(虽然发生的概率很小) 。

参考文章

1. 《Android进阶之光》

 历史上的今天

版权声明 1、 本站名称: 91易搜
2、 本站网址: 91es.com3xcn.com
3、 本站文章: 部分来源于网络,仅供站长学习和参考,若侵权请留言
广告也精彩

相关文章

广告也精彩

暂无评论

评论审核已启用。您的评论可能需要一段时间后才能被显示。

暂无评论...

网站升级中

公告

近期网站升级中,可能存在一些bug。欢迎反馈 https://www.91es.com/we.html

本站域名

本站域名 : 91es.com3xcn.com。本站邮箱 : 站长邮箱 i@oorr.cn,通知邮箱we@oorr.cn ,如有更新,请看公告 。