第5章 多线程编程
5.1 线程基础
5.1.1 如何创建线程
在java要创建线程,一般有==两种方式==:
1)继承Thread类
2)实现Runnable接口
1. 继承Thread类
继承Thread类,重写run方法,在run方法中定义需要执行的任务。
class MyThread extends Thread{
private static int num = 0;
public MyThread(){
num++;
}
@Override
public void run() {
System.out.println("主动创建的第"+num+"个线程");
}
}
创建好线程类之后,就可以创建线程对象了,然后通过start()方法去启动线程。不是调用run()方法启动线程,run方法中只是定义需要执行的任务,如果调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。
2. 实现Runnable接口
实现Runnable接口必须重写其run方法。
class MyRunnable implements Runnable{
public MyRunnable() {
}
@Override
public void run() {
System.out.println("子线程ID:"+Thread.currentThread().getId());
}
}
public class Test {
public static void main(String[] args) {
System.out.println("主线程ID:"+Thread.currentThread().getId());
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
这种方式必须将Runnable作为Thread类的参数,然后通过Thread的start方法来创建一个新线程来执行该子任务。如果调用Runnable的run方法的话,是不会创建新线程的,这根普通的方法调用没有任何区别。
实现Runnable接口相比继承Thread类有如下==优势==:
1、可以避免由于Java的单继承特性而带来的局限。
2、代码能够被多个线程共享,代码与数据是独立的,适合多个线程去处理同一资源的情况 。
5.1.2 线程的状态
线程状态线程包括以下这==7个状态==:创建(new)、就绪(runnable)、运行(running)、阻塞(blocked)、time wating、wating、消亡(dead)。
当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。
当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。
线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time wating、wating、阻塞。
当由于突然中断或者子任务执行完毕,线程就会被消亡。
5.1.3 上下文切换
对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做==线程上下文切换==(对于进程也是类似)。
由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。说简单点:对于线程的上下文切换实际上就是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。
5.1.4 理解中断
每一个线程都有一个用来表明当前线程是否请求中断的boolean类型标志,当一个线程调用interrupt()
方法时,线程的中断标志将被设置为true。我们可以通过调用Thread.currentThread().isInterrupted()
或者Thread.interrupted()
来检测线程的中断标志是否被置位。这两个方法的区别:前者是线程对象的方法,调用它后==不清除==线程中断标志位;后者是Thread的静态方法,调用它会==清除==线程中断标志位。
所以说调用线程的interrupt()
方法不会中断一个正在运行的线程,只是设置了一个线程中断标志位,如果在程序中不检测线程中断标志位,那么即使设置了中断标志位为true,线程也一样照常运行。
一般来说中断线程分为三种情况:
(1):中断非阻塞线程
(2):中断阻塞线程
(3):不可中断线程
1. 中断非阻塞线程
中断非阻塞线程通常有两种方式:
(1) 采用线程共享变量
这种方式比较简单可行,需要注意的一点是共享变量必须设置为volatile,这样才能保证修改后其他线程立即可见。
public class InterruptThreadTest extends Thread{
// 设置线程共享变量
volatile boolean isStop = false;
public void run() {
while(!isStop) {//无限循环一直执行
System.out.println(Thread.currentThread().getName() + "is running");
}
if (isStop) {//此时跳出无限循环,线程执行完毕
System.out.println(Thread.currentThread().getName() + "is interrupted");
}
}
public static void main(String[] args) {
InterruptThreadTest itt = new InterruptThreadTest();
itt.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 线程共享变量设置为true
itt.isStop = true;
}
}
(2) 采用中断机制
public class InterruptThreadTest2 extends Thread{
public void run() {
// 这里调用的是非清除中断标志位的isInterrupted方法
while(!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "is running");
}
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "is interrupted");
}
}
public static void main(String[] args) {
InterruptThreadTest2 itt = new InterruptThreadTest2();
itt.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 设置线程的中断标志位
itt.interrupt();
}
}
2. 中断阻塞线程
当线程调用Thread.sleep()、Thread.join()、object.wait()再或者调用阻塞的I/O操作方法时,都会使得当前线程进入阻塞状态。那么此时如果在线程处于阻塞状态下调用interrupt()方法会抛出一个异常,并且会清除线程中断标志位(设置为false)。这样一来线程就能退出阻塞状态。
代码实例如下:
public class InterruptThreadTest3 extends Thread{
public void run() {
// 这里调用的是非清除中断标志位的isInterrupted方法
while(!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " is running");
try {
System.out.println(Thread.currentThread().getName() + " Thread.sleep begin");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " Thread.sleep end");
} catch (InterruptedException e) {
//由于调用sleep()方法会清除状态标志位 所以这里需要再次重置中断标志位 否则线程会继续运行下去
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "is interrupted");
}
}
public static void main(String[] args) {
InterruptThreadTest3 itt = new InterruptThreadTest3();
itt.start();
try {
Thread.sleep(5000);//让出cpu
} catch (InterruptedException e) {
e.printStackTrace();
}
// 设置线程的中断标志位
itt.interrupt();
}
}
需要注意的地方就是 Thread.sleep()
、Thread.join()
、object.wait()
这些方法,会检测线程中断标志位,如果发现中断标志位为true则==抛出异常并且将中断标志位设置为false==。所以while循环之后每次调用阻塞方法后都要在捕获异常之后,调用Thread.currentThread().interrupt()
重置状态标志位。
3. 不可中断线程
有一种情况是线程不能被中断的,就是调用synchronized关键字获取到了锁的线程。
5.1.5 Thread类常用方法
1. sleep方法
sleep(long time)
sleep(long millis, int nanos)
==不会释放锁==,相当于让线程睡眠,让出CPU,必须处理InterruptedException异常。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。
2. yield方法
- `yield()
==不会释放锁==,不能控制具体的交出CPU的时间。直接用Thread类调用,让出CPU执行权给同等级的线程,如果没有相同级别的线程在等待CPU的执行权,则该线程继续执行。
注意,调用yield方法==并不会让线程进入阻塞状态,而是让线程重回就绪状态==,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
3. join方法
-
join()
等待thread执行完毕 -
join(long millis)
等待一定的时间 join(long millis,int nanoseconds)
==释放锁==,如果在一个线程A中调用另一个线程B的join方法,线程A将会等待线程B执行完毕后或等待一定的时间再执行。实际上调用join方法是调用了Object的wait()
方法。wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。
4. interrupt方法
interrupt()
即中断的意思。单独调用==interrupt方法可以使得处于阻塞状态的线程抛出一个异常==,也就说,它可以用来中断一个正处于阻塞状态的线程;直接调用interrupt方法不能中断正在运行中的线程。
5. 其他方法
-
getId()
用来得到线程ID -
getName()
和setName(String threadName)
用来得到或者设置线程名称。 -
getPriority()
和setPriority(int priority)
用来获取和设置线程优先级。 -
setDaemon(boolean isDaemon)
和isDaemon()
用来设置线程是否成为守护线程和判断线程是否是守护线程。 -
Thread.currentThread()
获取当前线程
守护线程和用户线程的区别:
守护线程:依赖于创建它的线程。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。 在JVM中,像垃圾收集器线程就是守护线程。
用户线程:不依赖创建它的线程,会一直运行完毕。
5.2 同步
5.2.1 同步方法
public synchronized void methodA() {
System.out.println("methodA.....");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
5.2.2 同步代码块
public void methodB() {
synchronized(this) {
System.out.pritntln("methodB.....");
}
}
5.2.3 volatite
1. Java内存模型
在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。
Java内存模型(本文简称为JMM)定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(JMM的一个抽象概念),本地内存中存储了该线程以读/写共享变量的副本。 线程对变量的所有操作都必须在本地内存中进行,而不能直接对主内存进行操作。并且每个线程不能访问其他线程的本地内存。
内存模型的抽象示意图从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
2. 并发编程的三个概念
并发编程需要处理的两个关键问题:线程通信和线程同步。
线程通信的两种方式:共享内存和消息传递。共享内存:线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。消息传递:线程之间必须通过明确的发送消息来显式进行通信。
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
(1) 原子性
一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
(2) 可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
(3) 有序性
程序执行的顺序按照代码的先后顺序执行。
指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
3. JAVA语言对于原子性,可见性,有序性的保证
原子性
基本数据类型的变量的读取和赋值操作是原子性操作
请分析以下哪些操作是原子性操作:
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到本地内存中。原子性操作。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入本地内存,虽然读取x的值以及将x的值写入本地内存这2个操作都是原子性操作,但是合起来就不是原子性操作了。同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
可见性
提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
有序性
可以通过volatile保证部分有序。
synchronized也可以保证有序性。
4. volatile关键字
volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是synchronized 的一部分。特点如下:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。volatile能在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。 - 不能保证原子性
只能在有限的一些情形下使用 volatile变量替代锁。要使volatile变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
(1) 状态标志
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
(2) 双重检查
class Singleton {
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
5.3 阻塞队列
为了更好的理解线程池,本节学习阻塞队列。
5.3.1 BlockingQueue
1. 认识BlockingQueue
- 在新增的Concurrent包中,高效且==线程安全==
- 用来==处理消费者生产者问题==。在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒。
2. 核心方法
-
put(anObject)
把anObject加到BlockingQueue里,如果没有空间,则调用此方法的线程被阻塞直到BlockingQueue里面有空间再继续。 -
offer(anObject)
存数据,如果可以容纳,则返回true,否则返回false(不阻塞当前执行方法的线程)。 -
offer(E o, long timeout, TimeUnit unit)
可以设定等待的时间,如果在指定的时间内,还不能往队列中加入,则返回失败。
-
take()
取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻塞进入等待状态直到BlockingQueue有新的数据被加入。 -
poll(time)
取走排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null。 -
poll(long timeout, TimeUnit unit)
取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。 -
drainTo()
一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数), 通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
5.3.2 BlockingQueue实现子类
公平锁:配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
非公平锁:配合一个LIFO队列来管理多余的生产者和消费者,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
1. ArrayBlockingQueue
- 基于数组实现,内部维护了一个定长数组和两个整形变量,分别缓存着队列中的数据对象及标识队列的头部和尾部在数组中的位置。生产和消费时不会产生或销毁任何额外的对象实例。
- 在放入数据和获取数据,都是共用同一个锁对象,两者==无法并行运行==。
- 在创建时,默认采用非公平锁。可以控制对象的内部锁是否采用公平锁。
2. LinkedBlockingQueue
- 基于链表实现,也维持着一个数据缓冲队列(该队列由一个链表构成)。生产和消费的时候会产生Node对象。
- 生产者端和消费者端分别采用了独立的锁来控制数据同步,高并发的情况下生产者和消费者==可以并行==地操作队列中的数据,以此来提高整个队列的并发性能。
- 构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
LinkedBlockingQueue和ArrayBlockingQueue的异同
相同:
最常用的阻塞队列,当队列为空,消费者线程被阻塞;当队列装满,生产者线程被阻塞;
区别:
a. 底层实现机制不同:LinkedBlockingQueue基于链表实现,在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大;而ArrayBlockingQueue内部维护了一个数组,在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例。
b. LinkedBlockingQueue中的消费者和生产者是不同的锁,而ArrayBlockingQueue生产者和消费者使用的是同一把锁;
c. LinkedBlockingQueue有默认的容量大小为:Integer.MAX_VALUE,当然也可以传入指定的容量大小;ArrayBlockingQueue在初始化的时候,必须传入一个容量大小的值。
3. PriorityBlockingQueue
基于优先级的==无界队列==,==存储的对象必须是实现Comparable接口==。队列通过这个接口的compare方法确定对象的priority。越小优先级越高,优先级越高,越优先取出。但需要注意的是PriorityBlockingQueue并==不会阻塞数据生产者==,而只会在没有可消费的数据时,==阻塞数据的消费者==。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
使用案例
4. DelayQueue
==无界队列==,只有当其指定的延迟时间到了,才能够从队列中获取到该元素。一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
使用场景:DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。
5. SynchronousQueue
- 一个不存储元素的阻塞队列,可以理解为容量为0。==每个插入(移除)操作必须等待另一个线程的移除(插入)操作==。可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
- 声明一个SynchronousQueue有两种不同的方式:公平锁和非公平锁。
SynchronousQueue<Integer> sc = new SynchronousQueue<>(true);//fair
,默认是不公平锁。
一个使用场景:
在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
由于SynchronousQueue是没有缓冲区的,所以如下方法不可用:
sc.peek();// Always returns null
sc.clear();
sc.contains(1);
sc.containsAll(new ArrayList<Integer>());
sc.isEmpty();
sc.size();
sc.toArray();
Integer [] in = new Integer[]{new Integer(2)};
sc.toArray(in);
sc.removeAll(new ArrayList<Integer>());
sc.retainAll(new ArrayList<Integer>());
sc.remove("a");
sc.peek();
不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。
SynchronousQueue 获取元素:
public class Main {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<Integer> sc = new SynchronousQueue<>(); // 默认不指定的话是false,不公平的
//sc.take();// 没有元素阻塞在此处,等待其他线程向sc添加元素才会获取元素向下执行
sc.poll();//没有元素不阻塞在此处直接返回null向下执行
sc.poll(5,TimeUnit.SECONDS);//没有元素阻塞在此处等待指定时间,如果还是没有元素直接返回null向下执行
}
}
SynchronousQueue 存入元素:
public class Main {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<Integer> sc = new SynchronousQueue<>(); // 默认不指定的话是false,不公平的
// sc.put(2);//没有线程等待获取元素的话,阻塞在此处等待一直到有线程获取元素时候放到队列继续向下运行
sc.offer(2);// 没有线程等待获取元素的话,不阻塞在此处,如果该元素已添加到此队列,则返回 true;否则返回 false
sc.offer(2, 5, TimeUnit.SECONDS);// 没有线程等待获取元素的话,阻塞在此处等待指定时间,如果该元素已添加到此队列,则返回true;否则返回 false
}
}
6. 总结
BlockingQueue不光实现了一个完整队列所具有的基本功能,同时在多线程环境下,他还自动管理了多线间的自动等待和唤醒功能,从而使得程序员可以忽略这些细节,关注更高级的功能。
5.4 线程池
线程池优点:
1) 重用线程池的线程,==减少线程创建和销毁带来的性能开销==
2) ==控制线程池的最大并发数==,避免大量线程互相抢系统资源导致阻塞
3) ==提供定时执行和间隔循环执行功能==
Android中的线程池的概念来源于Java中的Executor,Executor是一个接口,真正的线程池的实现为ThreadPoolExecutor。Android的线程池大部分都是通过Executor提供的工厂方法创建的。ThreadPoolExecutor提供了一系列参数来配制线程池,通过不同的参数可以创建不同的线程池。 而从功能的特性来分的话可以分成四类。
5.4.1 ThreadPoolExecutor
ThreadPoolExecutor是线程池的真正实现, 它的构造方法提供了一系列参数来配置线程池, 这些参数将会直接影响到线程池的功能特性。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
-
corePoolSize
: 线程池的核心线程数, 默认情况下, 核心线程会在线程池中一直存活, 即使都处于闲置状态. 如果将ThreadPoolExecutor#allowCoreThreadTimeOut
属性设置为true, 那么闲置的核心线程在等待新任务到来时会有超时的策略, 这个时间间隔由keepAliveTime
属性来决定 当等待时间超过了keepAliveTime
设定的值那么核心线程将会终止。 -
maximumPoolSize
: 线程池所能容纳的最大线程数, 当活动线程数达到这个数值之后, 后续的任务将会被阻塞。 -
keepAliveTime
: 非核心线程闲置的超时时长, 超过这个时长, 非核心线程就会被回收。 -
allowCoreThreadTimeOut
这个属性为true的时候, 这个属性同样会作用于核心线程。 -
unit
: 用于指定keepAliveTime参数的时间单位, 这是一个枚举, 常用的有TimeUtil.MILLISECONDS(毫秒), TimeUtil.SECONDS(秒)以及TimeUtil.MINUTES(分)。 -
workQueue
: 线程池中的任务队列, 通过线程池的execute方法提交的Runnable对象会存储在这个参数中。 -
threadFactory
: 线程工厂, 为线程池提供创建新线程的功能. ThreadFactory是一个接口。
1. ThreadPoolExecutor执行任务大致遵循规则
如果线程池中的线程数量未达到核心线程的数量, 那么会直接启动一个核心线程来执行任务.
如果线程池中的线程数量已经达到或者超过核心线程的数量, 那么任务会被插入到任务队列中排队等待执行.
如果在步骤2中无法将任务插入到任务队列中,这通常是因为任务队列已满,这个时候如果线程数量未达到线程池的规定的最大值, 那么会立刻启动一个非核心线程来执行任务.
如果步骤3中的线程数量已经达到最大值的时候, 那么会拒绝执行此任务,ThreadPoolExecutor会调用RejectedExecution方法来通知调用者。
2. AsyncTask的THREAD_POOL_EXECUTOR线程池配置
- 核心线程数等于CPU核心数+1
- 线程池最大线程数为CPU核心数的2倍+1
- 核心线程无超时机制,非核心线程的闲置超时时间为1秒
- 任务队列容量是128
5.4.2 线程池的分类
1. FixedThreadPool
通过Executor#newFixedThreadPool()
方法来创建。它是一种线程数量固定的线程池, 当线程处于空闲状态时, 它们并不会被回收, 除非线程池关闭了. 当所有的线程都处于活动状态时, 新任务都会处于等待状态, 直到有线程空闲出来. 由于FixedThreadPool只有核心线程并且这些核心线程不会被回收, 这意味着它能够更加快速地响应外界的请求.
2. CachedThreadPool
通过Executor#newCachedThreadPool()
方法来创建. 它是一种线程数量不定的线程池, 它只有非核心线程, 并且其最大值线程数为Integer.MAX_VALUE. 这就可以认为这个最大线程数为任意大了. 当线程池中的线程都处于活动的时候, 线程池会创建新的线程来处理新任务, 否则就会利用空闲的线程来处理新任务. 线程池中的空闲线程都有超时机制, 这个超时时长为60S, 超过这个时间那么空闲线程就会被回收.
和FixedThreadPool不同的是, CachedThreadPool的任务队列其实相当于一个空集合, 这将导致任何任务都会立即被执行, 因为在这种场景下SynchronousQueue是无法插入任务的. SynchronousQueue是一个非常特殊的队列, 在很多情况下可以把它简单理解为一个无法存储元素的队列. 在实际使用中很少使用.这类线程比较适合执行大量的耗时较少的任务
3. ScheduledThreadPool
通过Executor#newScheduledThreadPool()
方法来创建. 它的核心线程数量是固定的, 而非核心线程数是没有限制的, 并且当非核心线程闲置时会立刻被回收掉. 这类线程池用于执行定时任务和具有固定周期的重复任务
4. SingleThreadExecutor
通过Executor#newSingleThreadPool()
方法来创建. 这类线程池内部只有一个核心线程, 它确保所有的任务都在同一个线程中按顺序执行. 这类线程池意义在于统一所有的外界任务到一个线程中, 这使得在这些任务之间不需要处理线程同步的问题
5.5 Android的消息机制分析
出于性能优化的考虑,Android中UI的操作是线程不安全的。所以,Android规定:只有UI线程才能修改UI组件。
这样会导致新启动的线程无法修改UI,此时需要Handler消息机制。
5.5.1 ThreadLocal<T>的工作原理
ThreadLocal是一个线程内部的数据存储类,通过它可以在指定线程中存储数据,数据存储后,只有在指定线程中可以获取到存储的数据,对于其他线程来说无法获得数据。
1. 使用场景
(1) 当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。比如对于Handler来说,它需要获取当前线程的Looper,而Looper的作用域就是线程并且不同的线程具有不同的Looper,通过ThreadLocal可以轻松实现线程中的存取。
(2) 复杂逻辑下的对象传递。比如监听器的传递,有时候一个线程中的任务过于复杂,表现为函数调用栈比较深以及代码入口的多样性,而这时我们又希望监听器能够贯穿整个线程的执行过程。此时可以让监听器作为线程内的全局对象而存在,在线程内部只要通过get方法就可以获取到监听器。如果不采用ThreadLocal,只能采用函数参数的形式在栈中传递或作为静态变量供线程访问。第一种方式在调用栈很深时,看起来设计很糟糕,第二种方式不具有扩展性,比如同时N个线程并发执行。
2. 常用方法
-
set(T value)
设置到当前线程内部的ThreadLocal.ThreadLocalMap对象中的Entry[]数组的某个Entry中。Entry类似于一个Map,key是ThreadLocal对象,value是具体的值T,重复设置会覆盖。 -
get() T
循环当前线程内部的ThreadLocal.ThreadLocalMap对象中的Entry[]数组,取出当前对象的key对应的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocal.ThreadLocalMap对象
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
在不同线程访问同一个ThreadLocal对象,获得的值却是不同的。
5.5.2 MessageQueue的工作原理
用于存放Handler发送过来的消息。主要包含两个操作:插入和读取。读取操作本身会伴随着删除操作。内部通过一个单链表的数据结构来维护消息列表,因为其在插入和删除上的性能较高。插入和读取对应的方法分别是:enqueueMessage
和next
方法。
1. Message
线程之间传递的消息,可以携带少量数据
1)属性
-
what
用户自定义的消息码 -
arg1
携带整型数据 -
arg2
携带整型数据 -
obj
携带对象 -
replyTo
==Messenger==类型
2)方法
sendToTarget()
-
obtain() Message
从消息池中获取一个消息对象。不建议使用new Message()构造。 -
obtain(Message orign) Message
拷贝一个Message对象 -
obtain(Handler h, int what) Message
h:指定由谁处理,sendToTarget()
就是发给他。what:指定what属性。本质还是调用Handler.sendMessage进行发送消息 -
obtain(Handler h, Runnable callback) Message
callback:message被处理的时候调用 setData(Bundle data)
getData() Bundle
5.5.3 Looper的工作原理
每个线程的MessageQueue管家,一个线程对应一个Looper,一个MessageQueue(创建Looper的时候创建)。Looper会不停地从MessageQueue中查看是否有新消息,如果有新消息就会立即处理,否则就一直阻塞在那里。
private static void prepare(boolean quitAllowed) {
...
//sThreadLocal是一个静态变量,保证了线程和Looper对象的一对一
//存一个Looper到线程中
sThreadLocal.set(new Looper(quitAllowed));
...
}
private Looper(boolean quitAllowed) {
//创建了一个消息队列
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
通过Looper.prepare()方法,创建了一个Looper,一个MessageQueue,再通过Looper.loop()开启消息循环。
public static void loop() {
...
for (;;) {//无限循环
...
//next()是一个无限循环方法,没有消息就阻塞,当有新消息,会返回这条消息并将其从单链表中移除
Message msg = queue.next();
...
//处理。msg.target是发送这条消息的Handler对象,这样Handler发送的消息最终又交给Handler来处理了
msg.target.dispatchMessage(msg);
...
}
}
loop()方法会调用MessageQueue#next()
方法来获取新消息,next()方法是一个无限循环的方法,如果消息队列中没有消息,那么next方法会一直阻塞在这里,这也导致loop方法一直阻塞在那里。当有新消息到来时,next()方法会返回这条消息并将其从单链表中移除。如果MessageQueue的next方法返回了新消息,Looper就会处理这条消息:msg.target.dispatchMessage(msg)
,这里的msg.target是发送这条消息的Handler对象,这样Handler发送的消息最终又交给Handler来处理了。
Looper提供quit()
和quitSafely()
来退出一个Looper,区别在于quit会直接退出Looper,而quitSafely会把消息队列中已有的消息处理完毕后才安全地退出。Looper退出后,这时候通过Handler发送的消息会失败,Handler的send方法会返回false。在子线程中,如果手动为其创建了Looper,在所有事情做完后,应该调用Looper的quit方法来终止消息循环,否则这个子线程就会一直处于等待状态;而如果退出了Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。
1.方法
-
Looper.getMainLooper() Looper
返回主线程上面的Looper -
Looper.myLooper() Looper
返回当前线程的Looper -
prepare()
为当前线程创建Looper对象,和关联的MessageQueue(主线程无需创建,已经有了) -
loop()
开始轮询,记得quit() -
quit()
此时Handler.sendMessage将会返回false -
quitSafely()
将已经在MessageQueue中的消息处理完,再结束 -
isCurrentThread()
boolean 是否是当前线程的Looper -
getThread()
Thread 返回对应的线程
5.5.4 Handler的工作原理
Handler用于发送Message或Runnable到Handler所在线程,进行执行或处理。
Handler发送过程仅仅是向消息队列中插入了一条消息。MessageQueue的next方法就会返回这条消息给Looper,Looper拿到这条消息就开始处理,最终消息会交给Handler的dispatchMessage()
来处理,这时Handler就进入了处理消息的阶段。
构造方法
...
mLooper = Looper.myLooper();//获取当前线程中保存的Looper对象,主要为了获取其中的mQueue
mQueue = mLooper.mQueue;
...
sendMessage(Message msg)
在mQueue中插入一个消息,跨线程通讯了
dispatchMessage(Message msg)
//handler处理消息的过程。由Looper#loop()调用,运行在Looper所在线程。若主动调用,就运行在调用的线程中。
public void dispatchMessage(Message msg) {
//Message#obtain(Handler h, Runnable callback)中的callback,Handler#handleMessage(Message msg)不会被执行
if(msg.callback != null){
handleCallback(msg);
} else {
//Handler(Callback callback)中的callback(接口,只有一个方法boolean handleMessage(Message msg))
if (mCallback != null) {
//返回值决定了Handler#handleMessage(Message msg)是否会被执行
if (mCallback.handleMessage(msg)){
return;
}
}
handleMessage(msg);
}
}
1. 方法
- 构造方法:
Handler()
用当前线程的Looper,若当前线程没有Looper,将抛出异常 - 构造方法:
Handler(Looper looper)
指定Looper - 构造方法:
Handler(Callback callback)
-
sendEmptyMessage(int what) boolean
发送一个仅仅包含what的Message,返回值表示是否成功插入到MessageQueue -
sendEmptyMessageAtTime(int what, long uptimeMillis) uptimeMillis
:指定时间发送 -
sendEmptyMessageDelayed(int what, long delayMillis) delayMillis
:延迟n秒发送 -
postDelayed(Runnable r, long delayMillis)
发送Runnable对象到消息队列中,将被执行在Handler所在的线程 removeCallbacks(Runnable r)
-
handleMessage(Message msg)
必须要重写的方法 removeMessages(int what)
obtainMessage(int what)
sendMessage(Message msg) boolean
-
dispatchMessage(Message msg)
在调用此方法所在线程直接执行
2. 使用步骤
①:调用Looper.prepare()为当前线程创建Looper对象(主线程不用创建,已经有了),然后Looper
.loop()
②:创建Handler子类的实例,重写handleMessages()方法,处理消息
3. HandlerThread
一个为了快速创建包含Looper的一个线程类, start()时就创建了Looper和MessageQueue对象(本质)。
- 构造方法:
HandlerThread(String name)
getLooper() Looper
quit()
quitSafely()
用法:
mCheckMsgThread = new HandlerThread("check-message-coming");
mCheckMsgThread.start();
mCheckMsgHandler = new Handler(mCheckMsgThread.getLooper()){...}
5.5.5 主线程的消息循环
Android的主线程就是ActivityThread,主线程的入口方法为main(String[] args),在main方法中系统会通过Looper.prepareMainLooper()来创建主线程的Looper以及MessageQueue,并通过Looper.loop()来开启主线程的消息循环。
ActivityThread通过ApplicationThread和AMS进行进程间通信,AMS以进程间通信的方式完成ActivityThread的请求后会回调ApplicationThread中的Binder方法,然后ApplicationThread会向H发送消息,H收到消息后会将ApplicationThread中的逻辑切换到ActivityTread中去执行,即切换到主线程中去执行。四大组件的启动过程基本上都是这个流程。
Looper.loop(),这里是一个死循环,如果主线程的Looper终止,则应用程序会抛出异常。那么问题来了,既然主线程卡在这里了
- 那Activity为什么还能启动;
- 点击一个按钮仍然可以响应?
问题1:startActivity的时候,会向AMS(ActivityManagerService)发一个跨进程请求(AMS运行在系统进程中),之后AMS启动对应的Activity;AMS也需要调用App中Activity的生命周期方法(不同进程不可直接调用),AMS会发送跨进程请求,然后由App的ActivityThread中的ApplicationThread会来处理,ApplicationThread会通过主线程线程的Handler将执行逻辑切换到主线程。重点来了,主线程的Handler把消息添加到了MessageQueue,Looper.loop会拿到该消息,并在主线程中执行。这就解释了为什么主线程的Looper是个死循环,而Activity还能启动,因为四大组件的生命周期都是以消息的形式通过UI线程的Handler发送,由UI线程的Looper执行的。
问题2:和问题1原理一样,点击一个按钮最终都是由系统发消息来进行的,都经过了Looper.loop()处理。 问题2详细分析请看原书作者的Android中MotionEvent的来源和ViewRootImpl。
5.6 Android中的线程
在Android中,线程的形态有很多种:
- AsyncTask 封装了线程池和Handler,主要为了方便开发者在子线程中更新UI,底层是线程池。
- HandlerThread 具有消息循环的线程,内部可以使用handler,底层是Thread。
- IntentService 一种Service,内部采用HandlerThread来执行任务,当任务执行完毕后IntentService会自动退出。由于它是一种Service,所以不容易被系统杀死,底层是Thread 。
操作系统中,线程是操作系统调度的最小单元,同时线程又是一种受限的系统资源(不可能无限产生),其创建和销毁都会有相应的开销。同时当系统存在大量线程时,系统会通过时间片轮转的方式调度每个线程,因此线程不可能做到绝对的并发,除非线程数量小于等于CPU的核心数。频繁创建销毁线程不明智,使用线程池是正确的做法。线程池会缓存一定数量的线程,通过线程池就可以避免因为频繁创建和销毁线程所带来的系统开销。
主线程也叫UI线程,作用是运行四大组件以及处理它们和用户交互。子线程的作用是执行耗时操作,比如I/O,网络请求等。从Android 3.0开始,主线程中访问网络将抛出异常。
5.6.1 Android中的线程形态
1. AsyncTask
AsyncTask是一种轻量级的异步任务类,封装了Thread和Handler,可以在线程池中执行后台任务,然后把执行的进度和最终的结果传递给主线程并更新UI。但并不适合进行特别耗时的后台任务,对于特别耗时的任务来说, 建议使用线程池。
abstract class AsyncTask<Params, Progress, Result>
- Params:入参类型
- Progress:后台任务的执行进度的类型
- Result:后台任务的返回结果的类型
如果不需要传递具体的参数, 那么这三个泛型参数可以用Void来代替。
(1) 四个核心方法
-
onPreExecute() void
在主线程执行, 在异步任务执行之前, 此方法会被调用, 一般可以用于做一些准备工作。 -
doInBackground(Params... params) Result
在线程池中执行, 此方法用于执行异步任务, 参数params表示异步任务的输入参数。 在此方法中可以通过publishProgress(Progress... values) void
方法来更新任务的进度,publishProgress()
方法会调用onProgressUpdate()
方法。另外此方法需要返回计算结果给onPostExecute()
-
onProgressUpdate(Progress... values) void
在主线程执行,当后台任务publishProgress()
时,会被调用。 -
onPostExecute(Result res) void
在主线程执行, 在异步任务执行之后, 此方法会被调用, 其中result参数是后台任务的返回值, 即doInBackground的返回值。
除了上述的四种方法,还有onCancelled()
, 它同样在主线程执行, 当异步任务被取消时调用,这个时候onPostExecute()则不会被调用.
(2) AsyncTask使用过程中的一些条件限制
- AsyncTask的类必须在主线程被加载, 这就意味着第一次访问AsyncTask必须发生在主线程。在Android 4.1及以上的版本已经被系统自动完成。
- AsyncTask的对象必须在主线程中创建。
- execute方法必须在UI线程调用。
- 不要在程序中直接调用onPreExecute(), onPostExecute(), doInBackground和onProgressUpdate()
- 一个AsyncTask对象只能执行一次, 即只能调用一次execute()方法, 否则会报运行时异常。
- AsyncTask采用了一个线程来串行的执行任务。 尽管如此在3.0以后, 仍然可以通过
AsyncTask#executeOnExecutor()
方法来并行执行任务。
(3) AsyncTask的工作原理
AsyncTask中有两个线程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一个Handler(InternalHandler), 其中线程池SerialExecutor用于任务的排列, 而线程池THREAD_POOL_EXECUTOR用于真正的执行任务, 而InternalHandler用于将执行环境从线程切换到主线程, 其本质仍然是线程的调用过程。
AsyncTask的排队过程:首先系统会把AsyncTask#Params参数封装成FutureTask对象, FutureTask是一个并发类, 在这里充当了Runnable的作用. 接着这个FutureTask会交给SerialExecutor#execute()方法去处理. 这个方法首先会把FutureTask对象插入到任务队列mTasks中, 如果这个时候没有正在活动AsyncTask任务, 那么就会调用SerialExecutor#scheduleNext()方法来执行下一个AsyncTask任务. 同时当一个AsyncTask任务执行完后, AsyncTask会继续执行其他任务直到所有的任务都执行完毕为止, 从这一点可以看出, 在默认情况下, AsyncTask是串行执行的。
5.6.2 HandlerThread
HandlerThread继承了Thread, 它是一种可以使用Handler的Thread, 它的实现也很简单, 就是run方法中通过Looper.prepare()来创建消息队列, 并通过Looper.loop()来开启消息循环, 这样在实际的使用中就允许在HandlerThread中创建Handler.
从HandlerThread的实现来看, 它和普通的Thread有显著的不同之处. 普通的Thread主要用于在run方法中执行一个耗时任务; 而HandlerThread在内部创建了消息队列, 外界需要通过Handler的消息方式来通知HandlerThread执行一个具体的任务. HandlerThread是一个很有用的类, 在Android中一个具体使用场景就是IntentService.
由于HandlerThread#run()是一个无线循环方法, 因此当明确不需要再使用HandlerThread时, 最好通过quit()或者quitSafely()方法来终止线程的执行.
5.6.3 IntentService
IntentSercie是一种特殊的Service,继承了Service并且是抽象类,任务执行完成后会自动停止,优先级远高于普通线程,适合执行一些高优先级的后台任务; IntentService封装了HandlerThread和Handler
onCreate方法自动创建一个HandlerThread,用它的Looper构造了一个Handler对象mServiceHandler,这样通过mServiceHandler发送的消息都会在HandlerThread执行;IntentServiced的onHandlerIntent方法是一个抽象方法,需要在子类实现,onHandlerIntent方法执行后,stopSelt(int startId)就会停止服务,如果存在多个后台任务,执行完最后一个stopSelf(int startId)才会停止服务。
参考文献
Java中的多线程你只要看这一篇就够了
java并发编程---如何创建线程以及Thread类的使用
Java中继承thread类与实现Runnable接口的区别
网友评论