Android多线程介绍

Android  小知识  2023年7月5日 am8:08发布1年前 (2023)更新 91es.com站长
177 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. 2021: [摘]Android稳定性(二)bootup fail(0条评论)
  2. 2021: [摘]Android稳定性(一)SWT和ANR(0条评论)
  3. 2021: 张悦然:旧时光是个美人(0条评论)
  4. 2020: [摘]Java IO流输入输出流(0条评论)
  5. 2019: 朱自清 :绿(0条评论)
版权声明 1、 本站名称: 91易搜
2、 本站网址: 91es.com3xcn.com
3、 本站内容: 部分来源于网络,仅供学习和参考,若侵权请留言
3、 本站申明: 个人流水账日记,内容并不保证有效

暂无评论

暂无评论...

随机推荐

老舍:春风

济南与青岛是多么不相同的地方呢!一个设若比作穿肥袖马褂的老先生,那一个便应当是摩登的少女。可是这两处不无相似之点。拿气候说吧,济南的夏天可以热死人,而青岛是有名的避暑所在;冬天,济南也比青岛冷。但是,两地的春秋颇有点相同。济南到春天多风,青岛也是这样;济南的秋天是长而晴美,青岛亦然。对于秋天,我不...

Files中启动自己的播放器

前言记录一下点击Android Files文件管理器中多媒体文件拉起我们自己写的播放器。流水账,没啥可看的,跳过吧。正文流水账而已,记录一下,方便自己查阅。只需要在AndroidMainfest.xml中的Activity中配置如下隐藏内容!评论可看后才能查看!评论可看audio/*...

鲁迅:两地书(节选)

广平兄:仿佛记得收到来信有好几天了,但是今天才能写回信。“一步步的现在过去”,自然可以比较的不为环境所苦,但“现在的我”中,既然“含有原来的我”,而这“我”又有不满于时代环境之心,则苦痛也依然相续。不过能够随遇而安——即有船坐船云云——则比起幻想太多的人们来,可以稍为安稳,能够敷衍下去而已。总之,...

NDK中jni.h头文件完整内容

前言这里摘抄一下jni.h头文件中的所有内容,主要是方便自己查阅。涉及的文件android-ndk-r21d-windows-x86_64\android-ndk-r21d\toolchains\llvm\prebuilt\windows-x86_64\sysroot\usr\include...

Android消息机制之三Handler分析

接着《Android消息机制之一简介(1)》和《Android消息机制之二简介(2)》,我们现在来单独看看Handler源码。设计代码的路径:base\core\java\android\os\Handler.javaHandler的简单使用在项目中,Handler的声明和初始化一般...

冰心 : 一日的春光

去年冬末,我给一位远方的朋友写信,曾说我要尽量地吞咽今年北平的春天。今年北平的春天来得特别晚,而且在还不知春在哪里的时候,抬头忽见黄尘中绿叶成阴,柳絮乱飞,才晓得在厚厚的尘沙黄幕之后,春还未曾露面,已悄悄地远行了。天下事都是如此——去年冬天是特别地冷,也显得特别地长。每天夜里,灯下孤坐,听着扑...