安卓

Android线程

2019-06-12  本文已影响0人  VitaAin

一、基础概念

1 线程与进程

进程

当一个程序第一次启动的时候,Android会启动一个Linux进程和一个主线程。默认的情况下,所有该程序的组件都将在该进程和线程中运行。 同时,Android会为每个应用程序分配一个单独的Linux用户。

Android会尽量保留一个正在运行进程,只在内存资源出现不足时,Android会尝试停止一些进程从而释放足够的资源给其他新的进程使用, 也能保证用户正在访问的当前进程有足够的资源去及时地响应用户的事件。

可以将一些组件运行在其他进程中,并且可以为任意的进程添加线程。
组件运行在哪个进程中是在AndroidManifest.xml文件里设置的,用process属性来指定该组件运行在哪个进程中。设置这个属性,使每个组件均在各自的进程中运行,或者使一些组件共享一个进程,而其他组件则不共享。 此外,还可以设置 android:process,使不同应用的组件在相同的进程中运行,但前提是这些应用共享相同的 Linux 用户 ID 并使用相同的证书进行签署
<application>元素也有一个process属性,用来指定所有的组件的默认属性。

线程

线程是系统分配处理器时间资源的基本单元,也是系统调用的基本单位

线程与进程的区别和联系

简单来说:
一个程序包含进程,进程又包含线程,线程是进程的一个组成部分,进程是操作系统分配资源的基本单位,线程是不会分配资源的,一个进程可以包含多个线程,然后这些线程共享进程的资源。

2 并行与并发

并行

真正意义上的同时进行多种事情,这种只可以在多核CPU的基础下完成。即多个处理器或者多核处理器同时执行多个不同的任务。

并发

从宏观方面来说,并发就是同时进行多种任务,实际上,这几种任务,并不是同时进行的,而是交替进行的,而由于CPU的运算速度非常的快,会造成我们的一种错觉,就是在同一时间内进行了多种事情。即一个处理器处理多个任务。

3 线程状态

注意:当线程在runnable状态时是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable

4 原子性、可见性、有序性

原子性

一个操作或者一系列操作,要么全部执行要么全部不执行。数据库中的“事物”就是个典型的原子操作。

可见性

当一个线程修改了共享属性的值,其它线程能立刻看到共享属性值的更改。
比如JMM分为主存和工作内存,共享属性的修改过程是在主存中读取并复制到工作内存中,在工作内存中修改完成之后,再刷新主存中的值。若线程A在工作内存中修改完成但还没来得及刷新主存中的值,这时线程B访问该属性的值仍是旧值。这样可见性就没法保证。

有序性

程序执行的顺序按照代码的先后顺序执行。
为了提高性能,编译器和处理器都会对代码进行重新排序,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。(因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行)
指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确


二、基本使用

1 哪些大的地方是执行在主线程的

2 使用子线程的方式

注意:

1)使用工作线程(后台线程)时可能会遇到另一个问题,即:运行时配置变更(例如,用户更改了屏幕方向)导致 Activity 意外重启,这可能会销毁工作线程。

2)使用Thread和HandlerThread时,为了使效果更好,建议设置Thread的优先级偏低一点:Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
因为如果没有做任何优先级设置的话,新建的Thread默认与UI Thread是具有同样优先级的,同样优先级的Thread,CPU调度上还是可能会阻塞掉UI Thread,从而导致ANR。

3 线程终止


三、线程安全

在并发的情况下,代码经过多线程使用,线程的调度顺序不影响任何结果。线程不安全就意味着线程的调度顺序会影响最终结果,比如某段代码不加事务去并发访问。


四、线程同步

通过人为控制和调度,保证共享资源的多线程访问成为线程安全,以保证结果的准确。

1 synchronized

分为方法同步、块同步两种情况。

方法同步

synchronized修饰方法时可以是静态方法、非静态方法,但不能是抽象方法、接口方法。

线程在执行同步方法时是具有排它性的。当任意一个线程进入到一个对象的任意一个同步方法时,这个对象的所有同步方法都被锁定了,在此期间,其他任何线程都不能访问这个对象的任意一个同步方法,直到这个线程执行完它所调用的同步方法并从中退出,从而导致它释放了该对象的同步锁之后。在一个对象被某个线程锁定之后,其他线程是可以访问这个对象的所有非同步方法的。

块同步

通过锁定一个指定的对象,来对代码块进行同步。

同步方法和同步块之间的相互制约只限于同一个对象之间,静态同步方法只受它所属类的其它静态同步方法的制约,而跟这个类的实例没有关系。如果一个对象既有同步方法,又有同步块,那么当其中任意一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其他线程无法在此时访问这个对象的同步方法,也不能执行同步块。

synchronized使用场景.png

以下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同步代码的方式分为两大派系:

  1. synchronized声明非静态方法、同步代码块的synchronized (this){}、synchronized (非this对象){}
    这三者持有锁的对象为实例对象(类的实例对象可以有很多个),线程想要执行该synchronized作用范围内的同步代码,需获得对象锁

  2. synchronized声明静态方法、同步代码块的synchronized (类.class){}
    这两者持有锁的对象为Class对象(每个类只有一个Class对象,而Class对象是Java类编译后生成的.class文件,它包含了与类有关的信息),线程想要执行该synchronized作用范围内的同步代码,需获得类锁

若synchronized同步方法(代码块)持有锁的对象不同,则多线程执行相应的同步代码时互不干扰;若相同,则获得该对象锁的线程先执行同步代码,其他访问同步代码的线程会被阻塞并等待锁的释放。

2 volatile

参考:https://www.cnblogs.com/dolphin0520/p/3920373.html

一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,就具备了两层语义:
  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  2)禁止进行指令重排序。

用volatile修饰之后:

  1. 使用volatile关键字会强制将修改的值立即写入主存;
  2. 使用volatile关键字后,当线程2进行修改xxx时,会导致线程1的工作内存中缓存变量xxx的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
  3. 由于线程1的工作内存中缓存变量xxx的缓存行无效,所以线程1再次读取变量xxx的值时会去主存读取。

那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

原理和实现机制

《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

3 重入锁(ReentrantLock)

ReentrantLock重入锁,是实现Lock接口的一个类,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞

支持重入性,需要解决两个问题:

  1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
  2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
公平锁与非公平锁

ReentrantLock支持两种锁:公平锁、非公平锁。公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。
公平锁是按照加锁顺序来的,非公平锁是不按顺序的,也就是说先执行lock方法的锁不一定先获得锁。
ReentrantLock的构造方法无参时是构造非公平锁;还提供了另外一种方式,可传入一个boolean值,true时为公平锁,false时为非公平锁。

什么时候应该使用 ReentrantLock ?

在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock “性能会更好”。

ReentrantLock 常用方法

4 阻塞队列(BlockingQueue)

包括:LinkedBlockingQueue、ArrayBlockingQueue。
ArrayBlockingQueue 是一个用数组实现的有界阻塞队列。

区别

LinkedBlockingQueue是一个线程安全的阻塞队列,实现了先进先出等特性。
可以指定容量,也可以不指定,不指定的话默认最大是Integer.MAX_VALUE。

LinkedBlockingQueue 类的常用方法

其中主要用到put()和take()方法:

5 原子变量(AtomicInteger)

AtomicInteger是JAVA原子操作的Interger类,线程安全,使用原子锁。
AtomicInteger通过一种线程安全的加减操作接口,也就是说当有多个线程操作同一个变量时,使用AtomicInteger不会导致变量出现问题,而且比使用 synchronized效率高。

常用方法:get()、set()、getAndIncrement()、getAndDecrement()等。

包名 java.util.concurrent.atomic,该包名下包含其它同步数值类 AtomicBoolean、AtomicLong等。

注意:创建AtomicInteger对象时是作为成员变量使用的,不要在局部区域使用此对象。

6 使用线程池进行管理及优化


五、线程死锁

多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些线程都将无法向前推进。

产生原因

避免死锁

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 的工作方法简述:

2 Handler

Handler + MessageQueue + Looper

1)Message:消息体,用于装载需要发送的对象。

2)MessageQueue

3)Handler

4)Looper

一个Handler对应一个Looper对象(但一个Looper可以有多个Handler),一个Looper对应一个MessageQueue对象,使用Handler生成Message,而一个Handler可以生成多个Message(Handler和Message时一对多的关系)。

Handler和Looper对象是属于线程内部的数据,不过也提供与外部线程的访问接口,Handler就是公开给外部线程的接口,用于线程间的通信。Looper是由系统支持的用于创建和管理MessageQueue的依附于一个线程的循环处理对象,而Handler是用于操作线程内部的消息队列的,所以Handler也必须依附一个线程,且只能是一个线程。

异步消息处理机制

Handler异步消息处理机制.png

当应用程序启动时,系统会自动为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拒绝策略:

常见线程池

上一篇 下一篇

猜你喜欢

热点阅读