小木箱成长营并发编程系列教程(排期中~):
并发编程 · 基础篇(中) · 三大分析法分析Handler
并发编程 · 提高篇(上) · Java并发关键字那些事
并发编程 · 提高篇(下) · Java锁安全性那些事
并发编程 · 高级篇(上) · Java内存模型那些事
并发编程 · 高级篇(下) · Java并发BATJ面试之谈
并发编程 · 实战篇 · android下载器实现
Tips: 关注微信公众号小木箱成长营,回复 "并发编程" 可免费获得并发编程思维导图
一、序言
Hello,我是小木箱,欢迎来到小木箱成长营并发编程系列教程,今天将分享并发编程 · 基础篇 · android线程那些事
android线程那些事主要分为三部分,第一部分是5W2H分析并发,第二部分是线程安全特性,第三部分是线程安全,最后一部分是结语。
其中,5W2H分析并发主要是针对并发提出了6个高价值的问题。
其中,线程基础主要分为五部分,第一部分是线程操作,第二部分是线程属性,第三部分是线程通信,第四部分是线程运行状态,最后一部分是生产者和消费者模型。
其中,线程安全主要分为五部分,第一部分是带着问题出发,第二部分是线程安全特性,第三部分是线程安全强度,第四部分是线程安全方案,最后一部分是UncaughtException兜底。
如果完全掌握小木箱成长营并发编程系列教程,那么任何人都能通过高并发相关的技术面试。
二、5W2H分析并发
首先我们聊聊并发基础的第一部分内容5W2H分析并发。我们根据5W2H法则按照What、Why、Where、How、How much五个维度提出了六个高价值问题
- 并发是什么?
- android为什么要用并发?
- android哪些地方用到并发?
- android如何实现多线程?
- android合理使用并发有什么收益?
- android盲目使用并发有什么风险?
下面,小木箱就带带着问题出发,带大家正式进入并发基础内容学习。
2.1 并发是什么?
首先我们聊一聊5W2H的What,Java Concurrency—并发。
并发、并行和串行
并发是指系统在同一时间段可同时处理多个任务,而同一时刻只有一个任务处于运行状态,和并发有两个接近的概念很容易被混淆,串行和并行,串行、并发和并行是相对于进程或多线程来说的。如下图是串行、并发和并行的执行时间图。
串行比较好理解,如上图所示串行是指线程A完成之后做线程B,以此类推,直到完成线程C,每个线程排队执行。下面我们着重看一下并发和并行。
并发是指一个或若干个 CPU 对多个进程或线程之间进行多路复用。简单说线程A先做Task,工作一段时间,线程B再做Task;
线程B执工作一段时间线程C再做Task,线程C工作一段时间,线程A重新执行Task。
以此类推,直到工作完成,看上去像是三个线程同时一起执行,但其实完全可以交给一个线程执行。
对于并发来说,线程A先执行一段时间,然后线程B再执行一段时间,接着线程C再执行一段时间。每个线程都轮流得到 CPU 的执行时间,并发只需要一个 CPU 即能够实现, 线程利用率最优。
并行则是指多个进程或线程同一时刻被执行,是真正意义上同时执行,并行必须要有多个 CPU 支持。
并行是A、B和C三个线程同时执行一个或多个Task,每线程负责一项Task,A、B和C三线程在同一时刻齐头并进地完成这些事情。
并行比串行和并发时间开销要小,但是由于线程A、线程B和线程C是同时执行的,需要三个 CPU 才能实现,一定程度影响机器性能。
用一句话总结就是:
串行是一个时间段内多个任务执行时,一个任务执行完才能执行另一个。
并行是指一个时间段内每个线程分配给独立的核心,线程同时运行。
而并发指的是一个时间段多个线程在单个核心运行,同一时间只能一个线程运行,系统不停切换线程,看起来像同时运行,实际上是线程不停切换。
同步和异步
除了串行、并行和并发以外,实际开发过程中,同学们经常将同步、异步混淆,下面简单对比一下同步和异步的区别。
如下图是同步和异步执行时间图,同步和异步与并发、并行、串行区别点在于同步和异步一般相对进程或多线程,而同步和异步一般是相对于线程而言的。
同步是指两个事物相互依赖,并且一个事物必须以依赖于另一事物的执行结果。比如在事物 A->B
事件模型中,你需要先完成事物 A 才能执行事物 B。
也就是说,同步调用在被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。
异步是指两个事物完全独立,一个事物的执行不需要等待另外一个事物的执行。也就是说,异步调用可以返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式带着调用结果再做相关事情。
阻塞和非阻塞
除了同步、异步以外,实际开发过程中,同学们经常将阻塞和非阻塞混淆,下面简单对比一下阻塞和非阻塞的区别。如下图是阻塞和非阻塞执行图
- 所谓
阻塞
是发出一个请求不能立刻返回响应,要等所有的逻辑全处理完才能返回响应。简单来说就是等待。 - 所谓
非阻塞
相反,发出一个请求立刻返回应答,不用等处理完所有逻辑。阻塞与非阻塞指的是单个线程内
遇到同步等待时,是否在原地不做任何操作。
那么同步阻塞、同步非阻塞、异步阻塞和异步非阻塞又有什么区别呢?
同步阻塞
同步阻塞是指在需要某资源时马上发起请求,并暂停本线程之后的程序,直至获得所需的资源。参考代码如下
输出结果:
小木箱成长营
同步非阻塞
同步非阻塞是指在需要某资源时马上发起请求,且可以马上得到答复,然后继续执行之后的程序。但如果得到的不是完整的资源,之后将周期性地的请求。参考代码如下:
输出结果:
小木箱正在学习并发编程
小木箱正在学习设计模式
异步阻塞
异步阻塞是指在需要某资源时不马上发起请求,而安排一个以后的时间再发起请求。当到了那时发出请求时,将暂停本线程之后的程序,直至获得所需的资源。参考代码如下:
输出结果:
小木箱成长营说: 异步任务开始 ...
小木箱成长营说: 异步任务结束 ...
小木箱成长营说: 所有异步任务执行完毕,继续执行后续任务
异步非阻塞
异步非阻塞是指在需要某资源时不马上发起请求,而安排一个以后的时间再发起请求。当到了那时发出请求时,可以马上得到答复,然后继续执行之后的程序。
但如果得到的不是完整的资源,之后将周期性地的请求。参考代码如下:
输出结果:
This is an asynchronous non-blocking code.
至此,同步、异步、阻塞、非阻塞以及他们的组合使用,小木箱已经讲解完毕了,下面小木箱着重的聊一下android为什么要使用并发?
2.2 android为什么要用并发?
因为CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,充分利用系统资源,让多个线程在同时运行的过程中竞争资源,充分利用android操作系统处理能力,因此我们需要使用并发。
总结一下就是: 提高程序性能、改善用户体验和节省设备资源
2.3 android哪些地方用到并发?
android并发场景应用非常广泛。
如果你需要编写启动器进行启动任务管理,那么你需要了解并发。
如果你需要对大文件进行多线程下载,那么你需要了解并发。
如果你用 AsyncTask进行调度任务,那么你需要了解并发。
如果你看Handler底层源码ThreadLocal实现,那么你需要了解并发。
如果你想编写一个合规的线程池,那么你需要了解并发。
......
所以说并发对android开发来说无影随形,既然并发对android程序员来说这么重要,那么我们该如何高效率使用并发呢?
2.4 android如何实现多线程?
android实现多线程的方式大概分为四种,第一种方式是HandlerThread,第二种方式是AsyncTask,第三种方式是IntentService,第四种方式是ExecutorService
2.4.1 HandlerThread
HandlerThread定义
首先,我们聊聊第一种方式HandlerThread,android的HandlerThread是一种特殊的Thread,HandlerThread提供了一个Looper,可以用来处理消息和处理程序。
HandlerThread优势在于可以将耗时的任务分发到后台线程,从而避免UI线程的阻塞和提高应用程序的性能和流畅性。
HandlerThread底层原理
android的HandlerThread底层原理:
- 创建一个继承自Thread类的HandlerThread,并重写run方法;
- 在run方法中创建一个Looper对象,并调用Looper.prepare方法;
- 在Looper.prepare方法中创建一个MessageQueue,并将MessageQueue赋值给Looper对象;
- 调用Looper.loop方法,HandlerThread会一直从MessageQueue中取出Message,并交给Handler处理;
- 如果HandlerThread调用了quit方法,那么Looper.loop方法就会停止,从而结束HandlerThread的运行。
因为,HandlerThread可以使用Handler来发送和处理消息并且HandlerThread可以创建多个Handler,每个Handler可以拥有自己的线程。所以,HandlerThread可以实现多线程。
因为,HandlerThread还提供了一种机制来管理线程,所以,线程可以在合适的时候被暂停或者恢复。
HandlerThread实现方式
下面,小木箱利用HandlerThread带大家实现一下android多线程:
2.4.2 AsyncTask
然后,我们聊聊第二种方式AsyncTask,android的AsyncTask通常用于执行一些短暂的耗时操作,比如从网络获取数据,在UI线程中执行简单的计算,或者更新UI等。
AsyncTask定义
android的AsyncTask是android提供的用于实现多线程的类,AsyncTask可以实现多线程协作,异步执行后台任务,并且可以通过主线程更新UI。
AsyncTask底层原理
android的AsyncTask实现多线程底层原理是:AsyncTask创建一个新的工作线程,在工作线程中调用doInBackground方法执行后台任务,同时在主线程中调用onProgressUpdate方法更新UI界面。
当doInBackground执行完毕后,会回调onPostExecute方法,在onPostExecute方法中可以更新UI界面。
AsyncTask使用方式
下面,小木箱利用android的AsyncTask带大家实现一下android多线程:
2.4.3 IntentService
IntentService定义
接着,我们聊聊第三种方式IntentService,IntentService是android提供的一种用于执行异步任务的服务,IntentService是一种特殊的Service,可以在单独的工作线程中处理耗时任务,并在完成后自动停止。
IntentService可以处理多个异步任务,每个任务都会在一个单独的线程中处理,因此不会阻塞UI线程,而且可以在任务完成后自动停止。
IntentService底层原理
IntentService底层原理是IntentService利用HandlerThread类来处理任务,HandlerThread内部有一个Looper,Looper会循环从消息队列中取出消息,每取出一条消息就会执行一次handleMessage方法。
在IntentService中,handleMessage方法会调用onHandleIntent方法,onHandleIntent方法就是我们要实现的任务,当任务执行完毕后,IntentService会自动停止。
IntentService使用方式
下面,小木箱带大家看一下IntentService多线程代码实现:
- 在androidManifest.xml文件中声明一个IntentService:
创建IntentService
调用IntentService
2.4.4 ExecutorService
ExecutorService定义
最后,我们聊聊第四种方式ExecutorService,ExecutorService是一个接口,ExecutorService提供了一种机制,可以将任务提交给Executor,然后由Executor在后台执行任务,从而提供并发性。
ExecutorService还提供了一种机制,可以管理运行中的任务和完成的任务,以及检查任务的执行状态。
ExecutorService底层原理
ExecutorService底层原理是使用了一个线程池来管理多个线程,并且可以控制线程的数量,提供了一系列的API来提交任务,并且可以控制任务的执行,比如可以提交一个任务,可以提交一个任务序列,可以提交一个可以控制任务执行时间的任务,也可以提交一个定时任务,实现了对任务的管理和控制。
ExecutorService的工作原理是,当调用其中的submit方法时,会将任务提交到线程池中,线程池会负责将任务分配给线程,然后线程池会控制线程的数量,如果线程数量超出了限制,则会把任务放到队列中,等待空闲的线程来执行任务, 如果没有空闲的线程,则会新建一个线程来执行任务,当线程完成任务时,会从队列中取出下一个任务来执行,直到所有的任务都完成,ExecutorService才会结束。
ExecutorService使用方式
下面,小木箱带大家看一下ExecutorService多线程代码实现:
最后,小木箱对HandlerThread、AsyncTask、IntentService和ExecutorService使用场景和优缺点做一下简单的归纳总结:
类型 | 使用场景 | 优点 | 缺点 |
HandlerThread | 需要在后台运行一个持续的线程,可以在线程中处理消息队列中的消息 | 可以实现消息的传递和处理,可以定义不同的消息处理程序 | 容易出现内存泄漏,资源消耗大 |
AsyncTask | 异步处理耗时操作 | 操作简单,实现快捷,可以很方便的在主线程和子线程之间传递消息 | 容易出现内存泄漏,资源消耗大,不能处理复杂的任务 |
IntentService | 后台处理长时间任务,处理结束后自动停止 | 可以处理复杂的任务,可以实现消息的传递和处理 | 资源消耗大,不能处理频繁的任务 |
ExecutorService | 异步处理耗时操作 | 操作简单,可以实现消息的传递和处理,可以处理复杂的任务 | 资源消耗大,不能处理频繁的任务 |
2.5 android合理使用并发有什么收益?
那么,android合理使用并发有什么收益?
当我们在使用多线程处理文件下载过程中,不颠覆原有线程池使用方式的基础之上,从降低线程池参数修改的成本以及多维度监控这两个方面可以降低故障发生的概率。
总结一下就是: 提高应用程序性能、改善用户体验、提高应用程序的可维护性和改善应用程序的可扩展性
2.6 android盲目使用并发有什么风险?
因为线程池的参数并不好配置。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;
另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,这导致业界并没有一些成熟的经验策略帮助开发人员参考。
如果盲目使用并发会导致如下三个问题:
- 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
- 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
- 系统无法合理管理内部的资源分布,会降低系统的稳定性。
总结一下就是: 内存泄漏、线程安全和数据不一致。
三、线程基础
3.1 线程操作
3.1.1 线程使用方式
创建线程有四种方式,第一种是直接 new Thread 重写Thread的run方法。
第二种是实现Runnable接口,将Runnable接口传给Thread。无论是继承Thread还是Runnable接口都无法获取任务执行结果。
如果需要获取任务执行结果,就需要使用第三种方式使用Callable和Future接口。
因为历史设计的原因,Thread只接受Runnable而不接受Callable,而FutureTask是Runnable和Callable的包装,FutureTask本身是继承Runnable的,所以FutureTask可以直接传给Thread,FutureTask调用get方法就可以获取到线程执行结果。
如果FutureTask任务没有找执行完,那么FutureTask无参get会一直阻塞,FutureTask可以使用超时get,超过一定时间就返回null。
第四种方式是线程池方式,本文简单入门一下,后文会着重讲解。
继承Thread类
输出结果:
小木箱说,当前运行的线程名为: CrazyCodingBoyThreadTest1
小木箱说,当前运行的线程名为: CrazyCodingBoyThreadTest2
实现Runnable接口
输出结果:
小木箱说,当前运行的线程名为: CrazyCodingBoyRunnable1
小木箱说,当前运行的线程名为: CrazyCodingBoyRunnable2
使用Callable和Future接口
输出结果:
小木箱说: 主线程在执行任务
小木箱说: Callable子线程开始计算
小木箱说: task运行结果4950
小木箱说: 所有任务执行完毕
使用Executors类
输出结果:
index:2
index:0
index:1
使用线程池
Executor管理多个异步任务的执行,是无需显式的管理线程的生命周期的。
输出结果:
pool-1-thread-4 Start. Command = 3
pool-1-thread-2 Start. Command = 1
pool-1-thread-3 Start. Command = 2
pool-1-thread-5 Start. Command = 4
pool-1-thread-1 Start. Command = 0
3.1.2 启动线程
启动线程的方式有两种,第一种是start,第二种是run,其中start才是启动线程的方法,run是一个普通方法。
3.1.2.1 start
start的线程处于就绪状态,当得到CPU的时间片后就会执行其中的run方法,具体可以看一下图示例代码,因为当执行到此处,创建了一个新的线程t并处于就绪状态,代码继续执行,打印出”ping”。此时,执行完毕。线程t得到CPU的时间片,开始执行,调用pong方法打印出”pong”。
3.1.2.2 run
通过run方法启动线程其实就是调用一个类中的方法。无需等待run方法中的代码执行完毕,就可以接着执行下面的代码。并没有创建一个线程,程序中依旧只有一个主线程,必须等到run方法里面的代码执行完毕,才会继续执行下面的代码,这样就没有达到写线程的目的。具体可以参考如下示例代码,因为t.run实际上就是等待执行new Thread里面的run方法调用pong完毕后,再继续打印”ping”。
思考1: 一个线程两次调用start方法会出现什么情况?为什么?
思考2: 既然 start 方法会调用 run 方法,为什么我们选择调用 start 方法,而不是直接调用 run 方法呢?
3.1.3 线程中断
说完启动线程,我们说一下线程中断。
什么是线程中断?当需求做到一半产品说要下线,就相当于线程中断。
什么是线程中断不了?当需求做到一半产品说要下线,但是你觉的产品SB,要继续做完,就相当于线程中断不了。
正常情况下线程执行完成自动结束。
如果运行时异常,会调用一个线程的interrupt方法来中断该线程。
如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程,中断会提前结束。
下面小木箱说一下线程人为中断的两种方式: stop和interrupt。
3.1.3.1 危险中断(不推荐)❌
- stop
因为stop方法线程中断很危险的,如果stop方法强行线程中断,那么会使一些清理工作得不到完成,导致资源泄露。
如果线程调用stop方法后导致线程持有的锁突然释放,那么数据会呈现不一致性,对象的内部状态因此被破坏。
stop方法不会保证线程立即终止,使用stop方法可能会导致线程死锁问题。
3.1.3.2 安全中断(推荐)✔️
interrupt方法是一个标识位,interrupt只是对线程打了一个“中断”的标记,并不是真正的停止线程。当线程进入到阻塞状态时,就会检查这个标记,如果被设置了,就会抛出InterruptedException,从而提前结束被阻塞状态。 如果线程处于正常活动状态时,如果检查到这个interrupt标记被设置了,那么线程将不会抛出InterruptedException,而是继续正常运行,除非线程在代码中去检查interrupt标记,然后自行决定如何处理。
interrupt线程中断实现
下面,小木箱带大家实现一下线程中断的逻辑
线程中断面试题
关于线程或线程池的中断有两个问题小木箱需要让大家思考一下。
问题一: interrupt、interrupted和isInterrupted有什么区别呢?
我们一般使用interrupted方法可以判断线程是否被中断,可以在循环体中使用interrupted方法判断条件,使用interrupt方法来提前中断线程。
问题二: 线程池是怎样中断的?
Executor的中断操作有两种: 第一种是通过shutdown方法实现。第二种是通过shutdownNow方法实现。
shutdown方法会等全部wait线程都执行完毕之后再关闭。
shutdownNow,相当于调用每个thread的interrupt方法。
3.1.4 线程切换
如果当前线程已经完成,那么我们可以利用yield切换到其他线程去执行
3.2 线程属性
线程操作小木箱说完了,接下来小木箱说一下线程属性,线程属性有三个,第一个是线程Id,第二个是线程名字,第三个是守护线程,第四个是线程优先级
首先我们看一下测试代码,分析一下线程属性:
输出结果:
true
main Thread's name is main
sub Thread's name is Thread-0
sub Thread is 22
main Thread id is 1
通过以上测试代码,我们可以得出结论:
3.2.1 线程ID
线程ID可以用来在线程之间传递消息,线程ID可以用来检查线程的状态,线程ID可以检查线程是否完成某些任务
3.2.2 线程名字
线程默认名字是0,Java中的线程名字是由Thread类的getName方法获取的,该方法返回一个字符串,表示线程的名字。
在创建线程时,可以使用Thread的构造函数来指定线程的名字,如果不指定,则系统会自动生成一个名字,格式为Thread-x,其中x是一个正整数。
3.2.3 守护线程
守护线程是程序运行在后台时提供Service的线程,当所有非守护线程结束时,程序终止,同时杀死所有的守护线程,守护线程不会占用太多的系统资源,通常会在后台运行。写测试类的时候main方法属于非守护线程,使用setDaemon方法可以将一个线程设置为守护线程 与非守护线程相比,守护线程拥有更低的优先级,并且在用户线程结束时自动结束。守护线程不能独立运行,而是需要依赖用户线程来执行任务,因此守护线程不能执行实际的任务,而只能为非守护线程提供服务。
3.2.4 线程优先级
线程优先级是指线程在多线程环境下的调度优先级,线程优先级决定了系统在多个线程之间进行调度时,哪个线程先执行,哪个线程后执行。线程优先级越高,越容易被调度,即被执行的概率越大,线程的优先级默认是5。
3.3 线程通信
实现线程协作的方式主要有四种,第一种是wait/notify/notifyAll方法。第二种是join方法。第三种是await/singal/singalAll方法。最后一种是CountDownLatch。
3.3.1 wait/notify/notifyAll
wait/ nofity/notifyAll方法是Object三个方法,详细可以参考API介绍Object有哪些公用方法?文章介绍,根据继承特性,所有Object子类都可以使用这wait/ nofity/notifyAll方法。
wait/ nofity/notifyAll方法只能在synchronized的同步代码块中使用,否则会抛出异常。
wait方法表示在其他线程调用此对象的 notify方法前,导致当前线程等待。
notify方法表示唤醒在此对象Monitor上等待的单个线程。
notifyAll方法表示唤醒在此对象Monitor上等待的所有线程。
Monitor是一种控制多线程同步互斥的机制,每一个Object实例都有一个Monitor与之相关联,每一个Monitor都有一个等待队列,
当调用wait方法时,当前线程就会进入到Monitor的等待队列中,等待被唤醒,当调用notify/notifyAll方法时,就会从Monitor的等待队列中唤醒一个或多个线程,使它们可以继续执行。
notify/notifyAll方法用于唤醒正在等待线程Monitor的线程,而Monitor则是一种控制多线程同步的机制,Monitor允许一个线程在其他线程执行操作之前或之后获得控制权,然后等待线程等到重新获得对Monitor控制权后才能继续执行。
那么线程如何成为该线程对象Monitor的控制者呢?一共有三种方法
- 使用synchronized关键字,当一个线程获得了某个对象的锁,该线程就成为了该对象的Monitor的控制者,直到它释放了该对象的锁。
- 使用Object.wait方法,当一个线程调用了某个对象的wait方法,该线程就成为了该对象的Monitor的控制者,直到它被唤醒或超时。
- 使用Lock接口,当一个线程获得了某个Lock实例的锁,该线程就成为了该Lock实例的Monitor的控制者,直到它释放了该Lock实例的锁。
注意: Monitor对象是共享的。Monitor对象可以保证在同一时间只有一个线程可以访问该资源,从而避免了多线程访问该资源时可能出现的竞争条件。
下面用wait 、 notify 和 notifyAll方法简单的实现一下线程协作:
输出结果:
Thread[#22,waitThreadA,5,main] wait !
Thread[#24,waitThreadC,5,main] wait !
Thread[#23,waitThreadB,5,main] wait !
Thread[#1,main,5,main] notify !
Thread[#1,main,5,main] notifyAll !
Thread[#24,waitThreadC,5,main] wait !
通过上述代码我们可以看到, 当线程A调用对象的wait方法时,线程A就会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。 当线程A调用对象的notify方法时,线程A就会唤醒等待此对象的线程B,线程B会进入对象锁定池准备获取对象锁进入运行状态。 当线程A调用对象的notifyAll方法时,线程A就会唤醒所有等待此对象的线程,这些线程都会进入对象锁定池准备获取对象锁进入运行状态。
当然wait和notify也是生产者-消费者的实现模型,具体事项细节可以参考 #5.1
3.3.2 join
然后说说第二种join方法,主线程需要获得子线程的执行结果,join方法的主要作用是等待调用该方法的线程终止。
当一个线程调用另一个线程的join方法时,调用线程将被阻塞,直到被调用的join方法所属的线程终止。
调用线程才继续执行,使所有线程都等待被join的线程终止,这样,才能确保某个线程在另一个线程之前终止。
假设有这样一个场景: 在主线程中启动了一个子线程做耗时工作,主线程会先于子线程结束, 如何主线程中获得子线程的结果?
我们可能会想到用sleep可以让主线程休眠,等子线程执行完了,再继续主线程的执行,但是休眠多久这是完全不知道的。而且sleep不会释放锁,可能会抛出InterruptedException,Future、CutDownLaunch和join都可以很方便地实现这个功能。
下面,小木箱用join方法等待线程终止去实现这个功能:
出结果:
小木箱说: 子线程开始运行
小木箱说: 子线程运行结束
小木箱说: 主线程继续运行
join三个线程协作,小木箱用代码实现一下:
输出结果:
小木箱成长营的产品经理规划新需求
小木箱开发新需求功能
小木箱成长营的测试测试新功能
3.3.3 await/singal/singalAll
接着说说第三种是await/singal/singalAll ,在Java中,除了Object的wait和notify/notify方法可以实现等待通知机制。
java.util.concurrent类中提供的Condition和Lock配合同样可以完成等待通知机制,Condition能够更加精确地控制多线程之间的协调与通信。
Condition对象关联一个锁对象,只有在获得与之关联的锁时,才能够调用Condition实例的await方法使线程等待,或者调用signal/signalAll方法发出通知唤醒等待的线程。
当一个线程调用Condition实例的await方法时,可以指定等待的条件,Condition就会释放与之关联的锁,同时进入等待状态,直到其它线程调用Condition实例的signal/signalAll方法时,该线程才会从等待状态中唤醒,并重新获得与之关联的锁。
下面用Condition实现三个线程依次打印ABC逻辑:
输出结果:
ABC
ABC
ABC
3.3.4 CountDownLatch
最后说说第三种CountDownLatch,CountDownLatch底层原理是利用可重入锁ReentrantLock和条件变量Condition,同时还有一个计数器count。
当count的值大于0时,表示还有任务没有完成,await方法会被阻塞;当count的值等于0时,表示所有任务已经完成,await方法会返回。
countDown方法会将count减1,当count减至0时,会唤醒await方法返回。
CountDownLatch适用场景,是用来进行多个线程的同步管理,线程调用了countDownLatch.await 之后,需要等待countDownLatch的信号countDownLatch.countDown ,在收到信号前,CountDownLatch不会往下执行。 下面,小木箱用代码实现三个线程依次打印ABC
输出结果:
小木箱成长营A
小木箱成长营B
小木箱成长营C
3.4 线程运行状态
线程运行状态图
线程通信小木箱说完了,接下来我们聊一下线程运行状态,线程运行状态可以参考以下线程运行状态图以及相关参数定义
线程运行状态
- Time waiting(睡眠)
- Thread.sleep
- Waiting(等待)
- 定义
- 等待其他thread显式的唤醒,否则不会被分配CPU时间进入方法
- 形式
- object.wait
- Thread.join
- LockSupport.park
- Blocked(阻塞)
- 等待获取一个排它锁,如果其他thread释放了lock就会结束此状态
线程挂起/恢复
挂起线程是指把正在运行的线程暂停,线程暂停后,线程处于阻塞状态,不会消耗CPU资源,但是线程的状态仍然是RUNNABLE,只是没有被调度到CPU上执行。
恢复线程是指把挂起的线程重新调度到CPU上执行,线程恢复后,线程处于就绪状态,可以被调度到CPU上执行,消耗CPU资源。
线程挂起/恢复方法有: join与sleep,wait与notify两组方法。
首先我们来说一下join与sleep,join线程是指用线程对象调用,如果在一个线程A中调用另一个线程B的join方法,那么线程A将会等待线程B执行完毕后再执行。
如果在A线程的代码中调用了join,那么线程A会被挂起直至线程b运行完为止才会继续运行。
我们有序调用notify和wait,先执行wait,再执行notify,就不会像suspend和resume一样产生死锁:
3.5 生产者消费者模型
线程运行状态小木箱说完了,接下来我们聊一下生产者消费者模型,生产者消费者模式是通过一个线程容器来解决生产者和消费者的强耦合问题。
常见的方式有wait / notify方法、 await / signal方法 、 BlockingQueue阻塞队列方法和Semaphore方法
3.5.1 wait/notify
wait/ notify实现底层原理解析参照3.1
下面,小木箱用wait/ notify实现一下生产者消费者模型代码:
输出结果:
小燕子 --> 女
小木箱 --> 男
小燕子 --> 女
小木箱 --> 男
3.5.2 ReentrantLock
ReentrantLock是锁的另一种表现形式,因为JVM天生就支持synchronized,ReentrantLock不是所有JDK版本都支持,而且synchronized不用担心没有释放锁导致死锁问题,JVM会确保锁的释放,因此除非下列情况建议使用ReentrantLock,否则我们一律使用synchronized实现线程同步
- ① 如果你想更好的处理死锁,那么ReentrantLock提供了可中断的锁申请
- ② 如果你想实现更复杂的线程同步,更好控制notify哪个线程,那么ReentrantLock提供了wait/notify/signal更多的方法,并结合Condition对ReentrantLock高级应用,支持多个条件变量
- ③ 如果你想实现更精确的线程控制,例如每个到来的线程都将排队等候,那么ReentrantLock具有公平锁功能可以帮助到你
- ④如果你想更好的实现多层线程同步,那么建议你利用ReentrantLock可重入锁能力
下面,小木箱用ReentrantLock实现一下生产者消费者模型代码:
ReentrantLock(实现lock接口)相对于synchronized多了三个高级功能:
高级功能1: 等待可中断
ReentrantLock第一个高级功能是ReentrantLock等待可中断,ReentrantLock类提供了一个lockInterruptibly方法,lockInterruptibly方法可以让一个线程在等待锁的过程中响应中断。
ReentrantLock等待可中断实现代码如下:
高级功能2: 公平锁
ReentrantLock第二个高级功能是ReentrantLock具有公平锁,公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到的原则,只有等前面的线程释放了锁,后面的线程才能获取到锁。 非公平锁是指多个线程获取锁的顺序没有任何规则,任何一个线程都有可能获得锁,和先来后到没有任何关系,这样可能导致某些线程一直拿不到锁,结果也就是不公平的了。
synchronized只能是非公平锁,而ReentrantLock既支持公平锁也支持非公平锁。
ReentrantLock非公平锁
ReentrantLock公平锁
高级功能3: ReentrantLock + Condition
ReentrantLock第三个高级功能是ReentrantLock可以绑定多个Condition通过多次newCondition可以获得多个Condition对象,简单的实现复杂的线程同步
3.5.4 BlockingQueue
BlockingQueue实现主要用于生产者-消费者队列,但BlockingQueue另外还支持 Collection 接口。
BlockingQueue是线程安全的,所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到排队方法的目的。
BlockingQueue以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,BlockingQueue的四种形式处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(null 或 false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只给定最大时间限制内阻塞。
下面,小木箱用BlockingQueue实现一下生产者消费者模型代码:
3.5.5 Semaphore
Semaphore底层原理是基于信号量底层原理,Semaphore是一种用于控制进程或线程访问共享资源的系统调用。
Semaphore通过计数器来统计可以访问共享资源的进程或线程的数量,当计数器的值大于0时,表示有可用的资源,允许进程或线程访问共享资源;
当计数器的值等于0时,表示没有可用的资源,不允许进程或线程访问共享资源。
Semaphore提供了P(Proberen)和V(Verhogen)两种操作,P操作使计数器减1,V操作使计数器加1。
下面,小木箱用Semaphore实现一下生产者消费者模型代码:
3.5.6 PipedInputStream / PipedOutputStream
PipedInputStream / PipedOutputStream两个类位于java.io包中,PipedInputStream / PipedOutputStream是解决同步问题的最简单的办法,一个线程将数据写入管道,另一个线程从管道读取数据,PipedInputStream / PipedOutputStream便构成了一种生产者/消费者的缓冲区编程模式。
PipedInputStream/PipedOutputStream只能用于多线程模式,PipedInputStream / PipedOutputStream用于单线程下可能会引发死锁。
在生产者和消费者之间建立一个管道,从结果上看出也可以实现同步,但一般不使用,因为缓冲区不易控制、数据不易封装和传输。
下面,小木箱用PipedInputStream/PipedOutputStream实现一个生产者和消费者模型: