[读书笔记]Android进阶之光-多线程编程

1,712

1. 线程基础

1.1 进程与线程的区别

进程:进程是操作系统结构的基础,是程序在一个数据集合上运行的过程,是系统进行资源分配和调度的基本单位。进程可以被看作程序的实体,是线程的容器。比如一个App Demo就是一个进程。

1.2 什么是线程?

比如一个QQ浏览器的进程,它里面运行了很多的自任务就是线程,是操作系统调度的最小单元,也叫作轻量级进程。一个进程中可以创建多个进程,这些线程都拥有各自的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量。

1.3 为何要使用多线程?

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

2. 与进程相比,线程创建和切换开销更小,同时多线程在数据共享方面效率非常高。 

3. 方便重复利用计算机的资源。 

4. 简化程序的结构,使程序便于理解和维护。

1.3.1 线程的状态

1. New:新建状态

2. Runnable:可运行状态,就绪状态,一旦调用start方法,线程就处于Runnable状态。

3. Blocked:阻塞状态,线程被锁阻塞,暂时不活动。

4. Waiting:等待状态,直到线程调度器重新激活它。

5. Timed waiting: 超时等待状态,和等待状态不同的是,它是可以在指定的时间自行返回的。

6. Terminated:终止状态

总结:

线程创建后,调用Thread 的 start方法,开始进入运行状态,当线程执行wait方法后,线程进入等待状态,进入等待状态的线程需要其他线程通知才能返回运行状态。超时等待相当于在等待状态加上了时间限制,如果超过时间限制,则线程返回运行状态。当线程调用到同步方法时,如果线程没有获得锁则进入到阻塞状态,当阻塞状态的线程获取到锁时则重新回到运行状态。当线程执行完毕或者遇到意外异常终止时,都会进入终止状态。

1.3.2 创建线程

(1)继承Thread类,重写run()方法

(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。 

(2)创建Thread子类的实例,即创建了线程对象。 

(3)调用线程对象的start()方法来启动该线程。

(2)实现Runnable接口,并实现该接口的run()方法

1 自定义类并实现Runnable接口,实现run方法

2 创建Thread子类的实例,用实现Runnable接口的对象作为参数实例化该Thread对象 

3 调用Thread 的 start 方法来启动该线程

(3)实现Callable接口,重写call()方法

callable和runnable区别?

  1. callable可以在任务接受后提供一个返回值,runnable无法提供这个功能

 2. callable中的call方法可以抛出异常,而runnable的run方法不能抛出异常 

 3. 运行callable可以拿到一个Future对象,Future对象表示异步计算的结果,它提供了检查计算是否完成的方法。可以使用Future来监视目标线程调用call方法的情况。但调用Future.get方法获取结果时,当前线程就会阻塞,直到call返回结果。

1.4 理解中断

当线程的run方法执行完毕,或者在方法中出现没有捕获的异常时,线程将终止。 当一个线程调用intercept方法时,线程的中断标识位将被置位,线程会不时的检测这个中断标识位,以判断线程是否应该被中断。要想直到线程是否被置位,可以调用Thread。currentThread。isInterrupted.

1.4.1 安全的终止线程

被中断的线程不一定会终止,中断线程是为了引起线程的注意,被中断的线程可以决定如何去响应中断。比较重要的线程则不会理会中断,大部分情况则是线程会将中断作为一个终止的请求。线程阻塞时,发现中断标识位为true,则会抛出InterruptedException异常,并且中断标识位会复位。在捕捉到异常时,最好要调用Thread。currentThread。interrupt来设置中断状态,让外界通过判断 Thread.currentThread() .isInterrupted()来判断是否终止线程还是继续下去。

为什么主线程要给10s时间? 防止主线程执行完了,子线程的检查中断来不及执行。

2. 同步

在多线程应用中,两个或者两个以上的线程需要共享对同一个数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通常被称为竞争条件。

2.1.1 重入锁与条件对象

Synchronized 关键字自动提供了锁以及相关的条件。 ReentranLock 保护代码块的结构如下所示:

这一结构确保任何时刻只有一个线程进入临界区,临界区就是在同一时刻只能有一个任务访问的代码区。一旦一个线程封锁了锁对象,其他任何线程都无法进入Lock语句,把解锁的操作放到finally中是十分必要的,如果临界区发生了异常,锁是必须要释放的,否则其他线程将永远被阻塞。

条件锁:进入临界区时,却发现在某一个条件满足之后,它才能执行,可以使用一个条件对象来管理那些已经获得一个锁,但是却不能做有用工作的线程。

条件对象的使用, 1.创建 aliPaylock.newCondition() 2.Condition.await 当条件不满足时,阻塞线程,释放锁 3.当条件满足时,condition.signalAll() //激活

效果****等同于:

每一个java对象都有一个锁,线程拿的就是这个对象的锁。可以调用同步方法来获得锁,也可以使用一个同步代码块获得锁。

2.1.2 volatile

如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

那么就会保证该域的可见性和有序性。

Java的内存模型

 java中的堆内存用来存储对象的实例,堆内存是被所有线程共享的运行时内存区域,局部变量这些都在栈内存中,因此,对象存在内存可见性的问题。而局部变量,方法定义的参数则不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。java内存模型定义了线程和贮存之间的抽象关系:线程之间的共享变量存在主存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。本地内存只是java内存模型的一个抽象概念。java内存模型控制线程之间的通信,它决定一个线程对主存共享变量的写入何时队另一个线程可见。

线程A与线程B之间若要通信的话,必须要经历下面两个步骤:

1. 线程A和线程A本地内存中更新过的共享变量刷新到主存中去

2. 线程B到主存中去读线程A之前已更新过的共享变量。

3. int i=3; 执行线程必须先在自己的工作线程中对变量i所在的缓存进行赋值操作,然后再写入主存当中,而不是直接将数据写入主存当中。

(1)原子性:对基本数据类型变量的读取和赋值操作时原子性操作,即这些操作时不可中断的。一个语句含有多个操作时,就不是原子性操作,只有简单的读取和赋值才是原子性操作。我们常用Atomic包里面的类来保证原子性。

(2)可见行:是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。当一个共享变量被volatile修饰时,它会保证修改的值立即呗更新到主存,所以对其他线程是可见的。普通变量时不能保证可见性的,当普通变量被修改之后,并不会立即被写入内存,何时被写入主存也是不确定的。当其他线程去读取该值时,此时主存中可能还是原来的旧值,这样就无法保证可见性。

(3)有序性

Java内存模型中允许编译器和处理器对指令进行重排序,虽然重排序过程不会影响到单线程执行的正确性,但会影响到多线程并发执行的正确性。可以使用volatile来保证有序性,除了volatile,也可以通过synchronized和Lock来保证有序性。

volatile关键字

当一个共享变量被volatile修饰之后,其就具备了两个含义,一个是县城修改了变量的值时,变量的新值对其他线程是立即可见的。就是不同线程着这个变量进行操作时,具有可见性。另一个含义是禁止使用指令重排序。

什么是重排序?

重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。

volatile 不保证原子性

自增操作不是原子性操作,即使volatile也无法保证对变量的操作是原子性的。

 为什么自增操作不是原子性操作?

因为自增操作分三个步骤:读值,+1,写内存。只要有多线程就有可能阻塞。

synchroniszed和volatile 比较?

volatile关键字能禁止指令重排序,因此volatile能保证有序性。volatile关键字禁止指令重排序有两个含义:一个当程序执行到volatile变量的操作时,在其前面的操作已经全部执行完毕,并且结果并且结果对后面的操作可见,在其后面的操作还没有进行;

正确使用volatile关键字

synchronized关键字可防止多个线程同时执行一段代码,那么这会很影响程序的执行效率。而volatile关键字在某些情况下的性能要优于synchronized。volatile是无法替代synchronize的,因为无法保证操作的原子性。如果严格遵循volatile的使用条件, 即变量真正独立于其他变量和自己以前的值, 在某些 情况下可以使用volatile代替synchronized来简化代码

使用volatile必须具备以下两个条件:

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

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

volatile 有很多场景:

1. 用作状态标志

因为volatile能保证可见性和禁止重排序。

2. 双重检查模式(DCL)

3. 阻塞队列

3.1.1 阻塞队列简介

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

1. 常见阻塞场景

1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞,直到有数据放入队列。

2. 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞,直到队列中有空位置,线程被自动唤醒。

2. BlockingQueue的核心方法

1. offer(object): 将Object加到BlockingQueue里。

2. offer(E o,long timeout,TimeUnit unit):可以设定等待时间,如果在指定时间内还不能往队列里加入Object,则返回失败。

3. put,如果BlockingQueue没有空间,就阻塞。

4. poll:取排在首位的对象,如果不能立即去除,则指定时间到了就返回null。

5. poll(time):取出排在首位的对象,可以等待一定时间,如果时间到了还没取出,则返回失败。

6. take(): 取出排在首位的数据对象,如果没有可取的对象,则进入等待状态。

7. drainTo(): 一次性可以取出所有可用的数据对象。

3. Java中的阻塞队列

  • BlockingQueue 阻塞队列是线程安全的,在我们业务中是会经常频繁使用到的,如典型的生产者消费的场景,生产者只需要向队列中添加,而消费者负责从队列中获取。

  • 如上图展示,我们生产者线程不断的put 元素到队列,而消费者从中take 出元素处理,这样实现了任务与执行任务类之间的解耦,任务都被放入到了阻塞队列中,这样生产者和消费者之间就不会直接相互访问实现了隔离提高了安全性。

BlockingQueue 主要有下面六个实现类,分别是 ArrayBlockingQueueLinkedBlockingQueueSynchronousQueueDelayQueuePriorityBlockingQueueLinkedTransferQueue 。这些阻塞队列有着各自的特点和适用场景

下面各队列使用方法一致,只是特性不一致,所以我们只需掌握各个特性就可以了。

ArrayBlockingQueue

  • ArrayBlockingQueue 是一个我们常用的典型的有界队列,其内部的实现是基于数组来实现的,我们在创建时需要指定其长度,它的线程安全性由 ReentrantLock 来实现的。

    public ArrayBlockingQueue(int capacity) {...} public ArrayBlockingQueue(int capacity, boolean fair) {...}

  • 如上所示,ArrayBlockingQueue 提供的构造函数中,我们需要指定队列的长度,同时我们也可以设置队列是都是公平的,当我们设置了容量后就不能再修改了,符合数组的特性,此队列按照先进先出(FIFO)的原则对元素进行排序。

  • ReentrantLock一样,如果 ArrayBlockingQueue被设置为非公平的,那么就存在插队的可能;如果设置为公平的,那么等待了最长时间的线程会被优先处理,其他线程不允许插队,不过这样的公平策略同时会带来一定的性能损耗,因为非公平的吞吐量通常会高于公平的情况。

LinkedBlockingQueue

从它的名字我们可以知道,它是一个由链表实现的队列,这个队列的长度是 Integer.MAX_VALUE ,这个值是非常大的,几乎无法达到,对此我们可以认为这个队列基本属于一个无界队列(也又认为是有界队列)。此队列按照先进先出的顺序进行排序

PriorityBlockingQueue

  • PriorityBlockingQueue是一个支持优先级排序的无界阻塞队列,可以通过自定义实现 compareTo()方法来指定元素的排序规则,或者通过构造器参数 Comparator来指定排序规则。但是需要注意插入队列的对象必须是可比较大小的,也就是 Comparable的,否则会抛出 ClassCastException异常。
  • 它的 take方法在队列为空的时候会阻塞,但是正因为它是无界队列,而且会自动扩容,所以它的队列永远不会满,所以它的 put方法永远不会阻塞,添加操作始终都会成功

DelayQueue

  • DelayQueue 是一个实现PriorityBlockingQueue的延迟获取的无界队列。具有“延迟”的功能。

  • DelayQueue 应用场景:1. 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。2. 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。

  • 它是无界队列,放入的元素必须实现 Delayed接口,而 Delayed接口又继承了 Comparable接口,所以自然就拥有了比较和排序的能力,代码如下:

    public interface Delayed extends Comparable<Delayed> {
        long getDelay(TimeUnit unit);
    }
    
  • 可以看出 Delayed接口继承 Comparable,里面有一个需要实现的方法,就是 getDelay。这里的 getDelay方法返回的是“还剩下多长的延迟时间才会被执行”,如果返回 0 或者负数则代表任务已过期。

  • 元素会根据延迟时间的长短被放到队列的不同位置,越靠近队列头代表越早过期。

阻塞队列的实现原理

put:

先获取锁->判断当前元素个数是否等于数组的长度,如果相等,则调用notFull.await()进行等待。当此线程被其他线程唤醒时,通过enqueue方法插入元素。插入成功之后通过notEmpty.signal()唤醒正在等待取元素的线程。

take:

跟put方法类似,put等待的是notFull信号,而take等待的是notEmpty。 take中调用dequeue方法取得元素。

4. 线程池

4.1 ThreadPoolExecutor

corePoolSize:核心线程数。 默认情况下线程池是空的, 只有任务提交时才会创建线程。 如果当前运 行的线程数少于corePoolSize, 则创建新线程来处理任务; 如果等于或者多于corePoolSize, 则不再创建。

maximumPoolSize: 线程池允许创建的最大线程数。如果任务队列满了并且线程数小于 maximumPoolSize时, 则线程池仍旧会创建新的线程来处理任务。

keepAliveTime: 非核心线程闲置的超时时间。超过这个时间则回收。allowCoreThreadTImeOut属性为true时,keepAliveTime也会用到核心线程上。

TimeUnit:keepAliveTime参数的时间单位。DAYS,HOURS,MINUTES,SECONDS

workQueue:任务队列。如果当前线程数大于corePoolSize, 则将任务添加到此任务队列中。

ThreadFactory: 线程工厂

RejectedExecutionHandler: 饱和策略。

这是当任务队列和线程池都满了时所采取的应对策略, 默认 是AbordPolicy, 表示无法处理新任务, 并抛出RejectedExecutionException异常。 此外还有3种策略, 它们分 别如下。

(1) CallerRunsPolicy: 用调用者所在的线程来处理任务。 此策略提供简单的反馈控制机制, 能够减缓 新任务的提交速度。

(2) DiscardPolicy: 不能执行的任务, 并将该任务删除。

(3) DiscardOldestPolicy: 丢弃队列最近的任务, 并执行当前的任务

4.2 线程池的处理流程和原理

**
**

线程模型!

1.  如果线程池中的线程数未达到核心线程数。则创建核心线程去处理任务。

2. 如果线程数大于或者等于核心线程数,则将任务加入任务队列,线程池中的空闲线程会不断的从任务队列中取出任务进行处理。

3. 如果任务队列满了,并且线程数没有达到最大线程数,则创建非核心线程去处理任务。

4. 如果线程数超过了最大线程数,则执行饱和策略。

4.3 线程池的种类

CachedThreadPool

作用:创建一个可根据需要创建新线程的线程池。

CachedThreadPool的corepoolsize为0,maximumPoolSize设置为MAX_VALUE,这意味着CacheThreadPool没有核心线程,非核心线程是无界的。空闲线程等待新任务的最长时间为60S。用了阻塞队列SynchrousQueue,它是一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。

FixedThreadPool

当执行execute方法时,如果当前运行的线程数未达到corePoolSize(核心线程数)时,就创建核心线程来处理任务。如果达到了核心线程数,则将任务添加到LinkedBlockingQueue中。

FixedThreadPool就是一个有固定数量核心线程的线程池,并且这些核心线程不会被回收。当线程数超过corePoolSize时,就将任务存储在任务队列中;当线程池有空闲线程时,则去任务队列去取任务执行。

SingleThreadExecutor

只有一个核心线程!

当前运行的线程数未达到核心线程数,也就是当前没有运行的线程,则创建一个新线程来处理任务,如果当前有运行的线程,则将任务添加到阻塞队列。SingleThreadExecutor能确保所有的任务在一个线程中按照顺序逐一执行。

能实现定时和周期性任务的线程池。因为他的阻塞队列是DelayWorkQueue。

因为DelayedWorkQueue是无界的,所有MAX_VALUE这个参数是无效的。

5.AsyncTask的原理