并发编程 · 基础篇(上) · android线程那些事(1)

简介: 并发编程 · 基础篇(上) · android线程那些事

image.png

小木箱成长营并发编程系列教程(排期中~):

并发编程 · 基础篇(中) · 三大分析法分析Handler

并发编程 · 基础篇(下) · 三大分析法分析线程池

并发编程 · 提高篇(上) · Java并发关键字那些事

并发编程 · 提高篇(下) · Java锁安全性那些事

并发编程 · 高级篇(上) · Java内存模型那些事

并发编程 · 高级篇(下) · Java并发BATJ面试之谈

并发编程 · 实战篇 · android下载器实现

Tips: 关注微信公众号小木箱成长营,回复 "并发编程" 可免费获得并发编程思维导图

一、序言

Hello,我是小木箱,欢迎来到小木箱成长营并发编程系列教程,今天将分享并发编程 · 基础篇 · android线程那些事

android线程那些事主要分为三部分,第一部分是5W2H分析并发,第二部分是线程安全特性,第三部分是线程安全,最后一部分是结语。

其中,5W2H分析并发主要是针对并发提出了6个高价值的问题。

其中,线程基础主要分为五部分,第一部分是线程操作,第二部分是线程属性,第三部分是线程通信,第四部分是线程运行状态,最后一部分是生产者和消费者模型。

其中,线程安全主要分为五部分,第一部分是带着问题出发,第二部分是线程安全特性,第三部分是线程安全强度,第四部分是线程安全方案,最后一部分是UncaughtException兜底。

image.png

如果完全掌握小木箱成长营并发编程系列教程,那么任何人都能通过高并发相关的技术面试。

二、5W2H分析并发

首先我们聊聊并发基础的第一部分内容5W2H分析并发。我们根据5W2H法则按照What、Why、Where、How、How much五个维度提出了六个高价值问题

  • 并发是什么?
  • android为什么要用并发?
  • android哪些地方用到并发?
  • android如何实现多线程?
  • android合理使用并发有什么收益?
  • android盲目使用并发有什么风险?

下面,小木箱就带带着问题出发,带大家正式进入并发基础内容学习。

image.png

2.1 并发是什么?

传送门: Java Concurrency

首先我们聊一聊5W2H的What,Java Concurrency—并发。

image.png

并发、并行和串行

并发是指系统在同一时间段可同时处理多个任务,而同一时刻只有一个任务处于运行状态,和并发有两个接近的概念很容易被混淆,串行和并行,串行、并发和并行是相对于进程或多线程来说的。如下图是串行、并发和并行的执行时间图。

image.png

串行比较好理解,如上图所示串行是指线程A完成之后做线程B,以此类推,直到完成线程C,每个线程排队执行。下面我们着重看一下并发和并行。

image.png

并发是指一个或若干个 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 才能实现,一定程度影响机器性能。

用一句话总结就是:

串行是一个时间段内多个任务执行时,一个任务执行完才能执行另一个。

并行是指一个时间段内每个线程分配给独立的核心,线程同时运行。

而并发指的是一个时间段多个线程在单个核心运行,同一时间只能一个线程运行,系统不停切换线程,看起来像同时运行,实际上是线程不停切换。

image.png

同步和异步

除了串行、并行和并发以外,实际开发过程中,同学们经常将同步、异步混淆,下面简单对比一下同步和异步的区别。

如下图是同步和异步执行时间图,同步和异步与并发、并行、串行区别点在于同步和异步一般相对进程或多线程,而同步和异步一般是相对于线程而言的。

image.png

同步是指两个事物相互依赖,并且一个事物必须以依赖于另一事物的执行结果。比如在事物 A->B 事件模型中,你需要先完成事物 A 才能执行事物 B。

也就是说,同步调用在被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。

异步是指两个事物完全独立,一个事物的执行不需要等待另外一个事物的执行。也就是说,异步调用可以返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式带着调用结果再做相关事情。

阻塞和非阻塞

除了同步、异步以外,实际开发过程中,同学们经常将阻塞和非阻塞混淆,下面简单对比一下阻塞和非阻塞的区别。如下图是阻塞和非阻塞执行图

image.png

  • 所谓阻塞是发出一个请求不能立刻返回响应,要等所有的逻辑全处理完才能返回响应。简单来说就是等待。
  • 所谓非阻塞相反,发出一个请求立刻返回应答,不用等处理完所有逻辑。阻塞与非阻塞指的是单个线程内遇到同步等待时,是否在原地不做任何操作。

那么同步阻塞、同步非阻塞、异步阻塞异步非阻塞又有什么区别呢?

image.png

同步阻塞

同步阻塞是指在需要某资源时马上发起请求,并暂停本线程之后的程序,直至获得所需的资源。参考代码如下

image.png

输出结果:

小木箱成长营

同步非阻塞

同步非阻塞是指在需要某资源时马上发起请求,且可以马上得到答复,然后继续执行之后的程序。但如果得到的不是完整的资源,之后将周期性地的请求。参考代码如下:

image.png

输出结果:

小木箱正在学习并发编程

小木箱正在学习设计模式

异步阻塞

异步阻塞是指在需要某资源时不马上发起请求,而安排一个以后的时间再发起请求。当到了那时发出请求时,将暂停本线程之后的程序,直至获得所需的资源。参考代码如下:

image.png

输出结果:

小木箱成长营说: 异步任务开始 ...

小木箱成长营说: 异步任务结束 ...

小木箱成长营说: 所有异步任务执行完毕,继续执行后续任务

异步非阻塞

异步非阻塞是指在需要某资源时不马上发起请求,而安排一个以后的时间再发起请求。当到了那时发出请求时,可以马上得到答复,然后继续执行之后的程序。

但如果得到的不是完整的资源,之后将周期性地的请求。参考代码如下:

image.png

输出结果:

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底层原理:

  1. 创建一个继承自Thread类的HandlerThread,并重写run方法;
  2. 在run方法中创建一个Looper对象,并调用Looper.prepare方法;
  3. 在Looper.prepare方法中创建一个MessageQueue,并将MessageQueue赋值给Looper对象;
  4. 调用Looper.loop方法,HandlerThread会一直从MessageQueue中取出Message,并交给Handler处理;
  5. 如果HandlerThread调用了quit方法,那么Looper.loop方法就会停止,从而结束HandlerThread的运行。

image.png

因为,HandlerThread可以使用Handler来发送和处理消息并且HandlerThread可以创建多个Handler,每个Handler可以拥有自己的线程。所以,HandlerThread可以实现多线程。

因为,HandlerThread还提供了一种机制来管理线程,所以,线程可以在合适的时候被暂停或者恢复。

HandlerThread实现方式

下面,小木箱利用HandlerThread带大家实现一下android多线程:

image.png

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界面。

image.png

AsyncTask使用方式

下面,小木箱利用android的AsyncTask带大家实现一下android多线程:

image.png

2.4.3 IntentService

IntentService定义

接着,我们聊聊第三种方式IntentServiceIntentService是android提供的一种用于执行异步任务的服务,IntentService是一种特殊的Service,可以在单独的工作线程中处理耗时任务,并在完成后自动停止。

image.png

IntentService可以处理多个异步任务,每个任务都会在一个单独的线程中处理,因此不会阻塞UI线程,而且可以在任务完成后自动停止。

IntentService底层原理

IntentService底层原理是IntentService利用HandlerThread类来处理任务,HandlerThread内部有一个Looper,Looper会循环从消息队列中取出消息,每取出一条消息就会执行一次handleMessage方法。

在IntentService中,handleMessage方法会调用onHandleIntent方法,onHandleIntent方法就是我们要实现的任务,当任务执行完毕后,IntentService会自动停止。

IntentService使用方式

下面,小木箱带大家看一下IntentService多线程代码实现:

  • 在androidManifest.xml文件中声明一个IntentService:

image.png

创建IntentService

image.png

调用IntentService

image.png

2.4.4 ExecutorService

ExecutorService定义

最后,我们聊聊第四种方式ExecutorService,ExecutorService是一个接口,ExecutorService提供了一种机制,可以将任务提交给Executor,然后由Executor在后台执行任务,从而提供并发性。

image.png

ExecutorService还提供了一种机制,可以管理运行中的任务和完成的任务,以及检查任务的执行状态。

ExecutorService底层原理

ExecutorService底层原理是使用了一个线程池来管理多个线程,并且可以控制线程的数量,提供了一系列的API来提交任务,并且可以控制任务的执行,比如可以提交一个任务,可以提交一个任务序列,可以提交一个可以控制任务执行时间的任务,也可以提交一个定时任务,实现了对任务的管理和控制。

image.png

ExecutorService的工作原理是,当调用其中的submit方法时,会将任务提交到线程池中,线程池会负责将任务分配给线程,然后线程池会控制线程的数量,如果线程数量超出了限制,则会把任务放到队列中,等待空闲的线程来执行任务, 如果没有空闲的线程,则会新建一个线程来执行任务,当线程完成任务时,会从队列中取出下一个任务来执行,直到所有的任务都完成,ExecutorService才会结束。

image.png

ExecutorService使用方式

下面,小木箱带大家看一下ExecutorService多线程代码实现:

image.png

最后,小木箱对HandlerThread、AsyncTask、IntentService和ExecutorService使用场景和优缺点做一下简单的归纳总结:

类型 使用场景 优点 缺点
HandlerThread 需要在后台运行一个持续的线程,可以在线程中处理消息队列中的消息 可以实现消息的传递和处理,可以定义不同的消息处理程序 容易出现内存泄漏,资源消耗大
AsyncTask 异步处理耗时操作 操作简单,实现快捷,可以很方便的在主线程和子线程之间传递消息 容易出现内存泄漏,资源消耗大,不能处理复杂的任务
IntentService 后台处理长时间任务,处理结束后自动停止 可以处理复杂的任务,可以实现消息的传递和处理 资源消耗大,不能处理频繁的任务
ExecutorService 异步处理耗时操作 操作简单,可以实现消息的传递和处理,可以处理复杂的任务 资源消耗大,不能处理频繁的任务

2.5 android合理使用并发有什么收益?

那么,android合理使用并发有什么收益?

当我们在使用多线程处理文件下载过程中,不颠覆原有线程池使用方式的基础之上,从降低线程池参数修改的成本以及多维度监控这两个方面可以降低故障发生的概率。

总结一下就是: 提高应用程序性能、改善用户体验、提高应用程序的可维护性和改善应用程序的可扩展性

2.6 android盲目使用并发有什么风险?

因为线程池的参数并不好配置。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;

另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,这导致业界并没有一些成熟的经验策略帮助开发人员参考。

如果盲目使用并发会导致如下三个问题:

  1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
  2. 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
  3. 系统无法合理管理内部的资源分布,会降低系统的稳定性。

总结一下就是: 内存泄漏、线程安全和数据不一致。

三、线程基础

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方法就可以获取到线程执行结果。

image.png

如果FutureTask任务没有找执行完,那么FutureTask无参get会一直阻塞,FutureTask可以使用超时get,超过一定时间就返回null。

第四种方式是线程池方式,本文简单入门一下,后文会着重讲解。

 继承Thread类

image.png

输出结果:

小木箱说,当前运行的线程名为: CrazyCodingBoyThreadTest1

小木箱说,当前运行的线程名为: CrazyCodingBoyThreadTest2

 实现Runnable接口

image.png

输出结果:

小木箱说,当前运行的线程名为: CrazyCodingBoyRunnable1

小木箱说,当前运行的线程名为: CrazyCodingBoyRunnable2

 使用Callable和Future接口

image.png

输出结果:

小木箱说: 主线程在执行任务

小木箱说: Callable子线程开始计算

小木箱说: task运行结果4950

小木箱说: 所有任务执行完毕

 使用Executors类

image.png

输出结果:

index:2

index:0

index:1

 使用线程池

Executor管理多个异步任务的执行,是无需显式的管理线程的生命周期的。

image.png

输出结果:

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是一个普通方法。

image.png
  3.1.2.1 start

start的线程处于就绪状态,当得到CPU的时间片后就会执行其中的run方法,具体可以看一下图示例代码,因为当执行到此处,创建了一个新的线程t并处于就绪状态,代码继续执行,打印出”ping”。此时,执行完毕。线程t得到CPU的时间片,开始执行,调用pong方法打印出”pong”。

image.png

3.1.2.2 run

通过run方法启动线程其实就是调用一个类中的方法。无需等待run方法中的代码执行完毕,就可以接着执行下面的代码。并没有创建一个线程,程序中依旧只有一个主线程,必须等到run方法里面的代码执行完毕,才会继续执行下面的代码,这样就没有达到写线程的目的。具体可以参考如下示例代码,因为t.run实际上就是等待执行new Thread里面的run方法调用pong完毕后,再继续打印”ping”。

image.png

思考1: 一个线程两次调用start方法会出现什么情况?为什么?

思考2: 既然 start 方法会调用 run 方法,为什么我们选择调用 start 方法,而不是直接调用 run 方法呢?

3.1.3 线程中断

说完启动线程,我们说一下线程中断。

什么是线程中断?当需求做到一半产品说要下线,就相当于线程中断。

什么是线程中断不了?当需求做到一半产品说要下线,但是你觉的产品SB,要继续做完,就相当于线程中断不了。

正常情况下线程执行完成自动结束。

如果运行时异常,会调用一个线程的interrupt方法来中断该线程。

如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程,中断会提前结束。

下面小木箱说一下线程人为中断的两种方式: stop和interrupt。

3.1.3.1 危险中断(不推荐)❌
  • stop

image.png

因为stop方法线程中断很危险的,如果stop方法强行线程中断,那么会使一些清理工作得不到完成,导致资源泄露。

如果线程调用stop方法后导致线程持有的锁突然释放,那么数据会呈现不一致性,对象的内部状态因此被破坏。

stop方法不会保证线程立即终止,使用stop方法可能会导致线程死锁问题。

3.1.3.2 安全中断(推荐)✔️

image.png

interrupt方法是一个标识位,interrupt只是对线程打了一个“中断”的标记,并不是真正的停止线程。当线程进入到阻塞状态时,就会检查这个标记,如果被设置了,就会抛出InterruptedException,从而提前结束被阻塞状态。 如果线程处于正常活动状态时,如果检查到这个interrupt标记被设置了,那么线程将不会抛出InterruptedException,而是继续正常运行,除非线程在代码中去检查interrupt标记,然后自行决定如何处理。

interrupt线程中断实现

下面,小木箱带大家实现一下线程中断的逻辑

image.png

线程中断面试题

关于线程或线程池的中断有两个问题小木箱需要让大家思考一下。

问题一: interrupt、interrupted和isInterrupted有什么区别呢?

image.png

我们一般使用interrupted方法可以判断线程是否被中断,可以在循环体中使用interrupted方法判断条件,使用interrupt方法来提前中断线程。

问题二: 线程池是怎样中断的?

Executor的中断操作有两种: 第一种是通过shutdown方法实现。第二种是通过shutdownNow方法实现。

shutdown方法会等全部wait线程都执行完毕之后再关闭。

shutdownNow,相当于调用每个thread的interrupt方法。

3.1.4 线程切换

如果当前线程已经完成,那么我们可以利用yield切换到其他线程去执行

3.2 线程属性

线程操作小木箱说完了,接下来小木箱说一下线程属性,线程属性有三个,第一个是线程Id,第二个是线程名字,第三个是守护线程,第四个是线程优先级

首先我们看一下测试代码,分析一下线程属性:

image.png

输出结果:

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

image.png

线程ID可以用来在线程之间传递消息,线程ID可以用来检查线程的状态,线程ID可以检查线程是否完成某些任务

3.2.2 线程名字

线程默认名字是0,Java中的线程名字是由Thread类的getName方法获取的,该方法返回一个字符串,表示线程的名字。

在创建线程时,可以使用Thread的构造函数来指定线程的名字,如果不指定,则系统会自动生成一个名字,格式为Thread-x,其中x是一个正整数。

3.2.3 守护线程

image.png

守护线程是程序运行在后台时提供Service的线程,当所有非守护线程结束时,程序终止,同时杀死所有的守护线程,守护线程不会占用太多的系统资源,通常会在后台运行。写测试类的时候main方法属于非守护线程,使用setDaemon方法可以将一个线程设置为守护线程 与非守护线程相比,守护线程拥有更低的优先级,并且在用户线程结束时自动结束。守护线程不能独立运行,而是需要依赖用户线程来执行任务,因此守护线程不能执行实际的任务,而只能为非守护线程提供服务。

3.2.4 线程优先级

image.png

线程优先级是指线程在多线程环境下的调度优先级,线程优先级决定了系统在多个线程之间进行调度时,哪个线程先执行,哪个线程后执行。线程优先级越高,越容易被调度,即被执行的概率越大,线程的优先级默认是5。

3.3 线程通信

实现线程协作的方式主要有四种,第一种是wait/notify/notifyAll方法。第二种是join方法。第三种是await/singal/singalAll方法。最后一种是CountDownLatch。

3.3.1 wait/notify/notifyAll

image.png

wait/ nofity/notifyAll方法是Object三个方法,详细可以参考API介绍Object有哪些公用方法?文章介绍,根据继承特性,所有Object子类都可以使用这wait/ nofity/notifyAll方法。

wait/ nofity/notifyAll方法只能在synchronized的同步代码块中使用,否则会抛出异常。

wait方法表示在其他线程调用此对象的 notify方法前,导致当前线程等待。

notify方法表示唤醒在此对象Monitor上等待的单个线程。

notifyAll方法表示唤醒在此对象Monitor上等待的所有线程。

image.png

Monitor是一种控制多线程同步互斥的机制,每一个Object实例都有一个Monitor与之相关联,每一个Monitor都有一个等待队列,

当调用wait方法时,当前线程就会进入到Monitor的等待队列中,等待被唤醒,当调用notify/notifyAll方法时,就会从Monitor的等待队列中唤醒一个或多个线程,使它们可以继续执行。

image.png

notify/notifyAll方法用于唤醒正在等待线程Monitor的线程,而Monitor则是一种控制多线程同步的机制,Monitor允许一个线程在其他线程执行操作之前或之后获得控制权,然后等待线程等到重新获得对Monitor控制权后才能继续执行。

那么线程如何成为该线程对象Monitor的控制者呢?一共有三种方法

  • 使用synchronized关键字,当一个线程获得了某个对象的锁,该线程就成为了该对象的Monitor的控制者,直到它释放了该对象的锁。
  • 使用Object.wait方法,当一个线程调用了某个对象的wait方法,该线程就成为了该对象的Monitor的控制者,直到它被唤醒或超时。
  • 使用Lock接口,当一个线程获得了某个Lock实例的锁,该线程就成为了该Lock实例的Monitor的控制者,直到它释放了该Lock实例的锁。

注意: Monitor对象是共享的。Monitor对象可以保证在同一时间只有一个线程可以访问该资源,从而避免了多线程访问该资源时可能出现的竞争条件

下面用wait 、 notify 和 notifyAll方法简单的实现一下线程协作:

image.png

输出结果:

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方法的主要作用是等待调用该方法的线程终止。

image.png

当一个线程调用另一个线程的join方法时,调用线程将被阻塞,直到被调用的join方法所属的线程终止。

调用线程才继续执行,使所有线程都等待被join的线程终止,这样,才能确保某个线程在另一个线程之前终止。

假设有这样一个场景: 在主线程中启动了一个子线程做耗时工作,主线程会先于子线程结束, 如何主线程中获得子线程的结果?

我们可能会想到用sleep可以让主线程休眠,等子线程执行完了,再继续主线程的执行,但是休眠多久这是完全不知道的。而且sleep不会释放锁,可能会抛出InterruptedException,Future、CutDownLaunch和join都可以很方便地实现这个功能。

下面,小木箱用join方法等待线程终止去实现这个功能:

image.png

出结果:

小木箱说: 子线程开始运行

小木箱说: 子线程运行结束

小木箱说: 主线程继续运行

join三个线程协作,小木箱用代码实现一下:

image.png

输出结果:

小木箱成长营的产品经理规划新需求

小木箱开发新需求功能

小木箱成长营的测试测试新功能

3.3.3 await/singal/singalAll

接着说说第三种是await/singal/singalAll ,在Java中,除了Object的waitnotify/notify方法可以实现等待通知机制。

java.util.concurrent类中提供的ConditionLock配合同样可以完成等待通知机制,Condition能够更加精确地控制多线程之间的协调与通信。

image.png

Condition对象关联一个锁对象,只有在获得与之关联的锁时,才能够调用Condition实例的await方法使线程等待,或者调用signal/signalAll方法发出通知唤醒等待的线程。

当一个线程调用Condition实例的await方法时,可以指定等待的条件,Condition就会释放与之关联的锁,同时进入等待状态,直到其它线程调用Condition实例的signal/signalAll方法时,该线程才会从等待状态中唤醒,并重新获得与之关联的锁。

下面用Condition实现三个线程依次打印ABC逻辑:

image.png

image.png

image.png

输出结果:

ABC

ABC

ABC

3.3.4 CountDownLatch

image.png

最后说说第三种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

image.png

输出结果:

小木箱成长营A

小木箱成长营B

小木箱成长营C

3.4 线程运行状态

线程运行状态图

线程通信小木箱说完了,接下来我们聊一下线程运行状态,线程运行状态可以参考以下线程运行状态图以及相关参数定义

image.png

image.png

线程运行状态

  • 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执行完毕后再执行。

image.png

如果在A线程的代码中调用了join,那么线程A会被挂起直至线程b运行完为止才会继续运行。

image.png

我们有序调用notify和wait,先执行wait,再执行notify,就不会像suspend和resume一样产生死锁:

image.pngimage.png

3.5 生产者消费者模型

线程运行状态小木箱说完了,接下来我们聊一下生产者消费者模型,生产者消费者模式是通过一个线程容器来解决生产者和消费者的强耦合问题。

image.png

常见的方式有wait / notify方法、 await / signal方法 、 BlockingQueue阻塞队列方法和Semaphore方法

3.5.1 wait/notify

image.png

wait/ notify实现底层原理解析参照3.1

下面,小木箱用wait/ notify实现一下生产者消费者模型代码:

image.png

image.pngimage.png

image.png

输出结果:

小燕子 --> 女

小木箱 --> 男

小燕子 --> 女

小木箱 --> 男

3.5.2 ReentrantLock

ReentrantLock是锁的另一种表现形式,因为JVM天生就支持synchronized,ReentrantLock不是所有JDK版本都支持,而且synchronized不用担心没有释放锁导致死锁问题,JVM会确保锁的释放,因此除非下列情况建议使用ReentrantLock,否则我们一律使用synchronized实现线程同步

image.png

  • ① 如果你想更好的处理死锁,那么ReentrantLock提供了可中断的锁申请
  • ② 如果你想实现更复杂的线程同步,更好控制notify哪个线程,那么ReentrantLock提供了wait/notify/signal更多的方法,并结合Condition对ReentrantLock高级应用,支持多个条件变量
  • ③ 如果你想实现更精确的线程控制,例如每个到来的线程都将排队等候,那么ReentrantLock具有公平锁功能可以帮助到你
  • ④如果你想更好的实现多层线程同步,那么建议你利用ReentrantLock可重入锁能力

下面,小木箱用ReentrantLock实现一下生产者消费者模型代码:

image.png

image.png

image.png

ReentrantLock(实现lock接口)相对于synchronized多了三个高级功能:

高级功能1: 等待可中断

ReentrantLock第一个高级功能是ReentrantLock等待可中断,ReentrantLock类提供了一个lockInterruptibly方法,lockInterruptibly方法可以让一个线程在等待锁的过程中响应中断。

image.png

ReentrantLock等待可中断实现代码如下:

image.png

高级功能2: 公平锁

image.png


ReentrantLock第二个高级功能是ReentrantLock具有公平锁,公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到的原则,只有等前面的线程释放了锁,后面的线程才能获取到锁。 非公平锁是指多个线程获取锁的顺序没有任何规则,任何一个线程都有可能获得锁,和先来后到没有任何关系,这样可能导致某些线程一直拿不到锁,结果也就是不公平的了。

synchronized只能是非公平锁,而ReentrantLock既支持公平锁也支持非公平锁。

ReentrantLock非公平锁

image.png

ReentrantLock公平锁

image.png

高级功能3: ReentrantLock + Condition

image.png

ReentrantLock第三个高级功能是ReentrantLock可以绑定多个Condition通过多次newCondition可以获得多个Condition对象,简单的实现复杂的线程同步

image.png

3.5.4 BlockingQueue

BlockingQueue实现主要用于生产者-消费者队列,但BlockingQueue另外还支持 Collection 接口。

image.png

BlockingQueue是线程安全的,所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到排队方法的目的。

BlockingQueue以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,BlockingQueue的四种形式处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(null 或 false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只给定最大时间限制内阻塞。

下面,小木箱用BlockingQueue实现一下生产者消费者模型代码:

image.png

image.png

image.png

3.5.5 Semaphore

Semaphore底层原理是基于信号量底层原理,Semaphore是一种用于控制进程或线程访问共享资源的系统调用。

image.png

Semaphore通过计数器来统计可以访问共享资源的进程或线程的数量,当计数器的值大于0时,表示有可用的资源,允许进程或线程访问共享资源;

当计数器的值等于0时,表示没有可用的资源,不允许进程或线程访问共享资源。

Semaphore提供了P(Proberen)和V(Verhogen)两种操作,P操作使计数器减1,V操作使计数器加1。

下面,小木箱用Semaphore实现一下生产者消费者模型代码:

image.png

3.5.6 PipedInputStream / PipedOutputStream

PipedInputStream / PipedOutputStream两个类位于java.io包中,PipedInputStream / PipedOutputStream是解决同步问题的最简单的办法,一个线程将数据写入管道,另一个线程从管道读取数据,PipedInputStream / PipedOutputStream便构成了一种生产者/消费者的缓冲区编程模式。

image.png

PipedInputStream/PipedOutputStream只能用于多线程模式,PipedInputStream / PipedOutputStream用于单线程下可能会引发死锁。

在生产者和消费者之间建立一个管道,从结果上看出也可以实现同步,但一般不使用,因为缓冲区不易控制、数据不易封装和传输。

下面,小木箱用PipedInputStream/PipedOutputStream实现一个生产者和消费者模型:

image.png


相关文章
|
21小时前
|
Java 数据库 Android开发
【专栏】Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理
【4月更文挑战第27天】本文探讨了Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理。通过案例分析展示了网络请求、图像处理和数据库操作的优化实践。同时,文章指出并发编程的挑战,如性能评估、调试及兼容性问题,并强调了多线程优化对提升应用性能的重要性。开发者应持续学习和探索新的优化策略,以适应移动应用市场的竞争需求。
|
21小时前
|
安全 调度 Python
探索Python中的并发编程:协程与多线程的比较
本文将深入探讨Python中的并发编程技术,重点比较协程与多线程的特点和应用场景。通过对协程和多线程的原理解析,以及在实际项目中的应用案例分析,读者将能够更好地理解两种并发编程模型的异同,并在实践中选择合适的方案来提升Python程序的性能和效率。
|
21小时前
|
SQL 开发框架 .NET
高级主题:Visual Basic 中的多线程和并发编程
【4月更文挑战第27天】本文深入探讨了Visual Basic中的多线程和并发编程,阐述了其基本概念,如何使用`System.Threading.Thread`类创建线程,以及借助`ThreadPool`、`Monitor`和`SyncLock`进行同步管理。文章还提到了多线程编程面临的挑战如竞态条件、死锁和资源竞争,并介绍了VB的异步编程、TPL和并发集合等高级技术。通过实例展示了多线程在文件处理、网络通信和图像处理中的应用,并给出了多线程编程的最佳实践。总之,理解并掌握VB的多线程和并发编程能有效提升应用程序的性能和响应能力。
|
21小时前
|
安全 API 数据库
【转】Android线程模型(AsyncTask的使用)
【转】Android线程模型(AsyncTask的使用)
10 1
|
21小时前
|
消息中间件 程序员 调度
Python并发编程:利用多线程提升程序性能
本文探讨了Python中的并发编程技术,重点介绍了如何利用多线程提升程序性能。通过分析多线程的原理和实现方式,以及线程间的通信和同步方法,读者可以了解如何在Python中编写高效的并发程序,提升程序的执行效率和响应速度。
|
21小时前
|
安全 Java
Java中的并发编程:理解并发性与线程安全
Java作为一种广泛应用的编程语言,在并发编程方面具有显著的优势和特点。本文将探讨Java中的并发编程概念,重点关注并发性与线程安全,并提供一些实用的技巧和建议,帮助开发人员更好地理解和应用Java中的并发机制。
|
21小时前
|
Dart 前端开发 安全
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
【4月更文挑战第30天】本文探讨了Flutter中线程管理和并发编程的关键性,强调其对应用性能和用户体验的影响。Dart语言提供了`async`、`await`、`Stream`和`Future`等原生异步支持。Flutter采用事件驱动的单线程模型,通过`Isolate`实现线程隔离。实践中,可利用`async/await`、`StreamBuilder`和`Isolate`处理异步任务,同时注意线程安全和性能调优。参考文献包括Dart异步编程、Flutter线程模型和DevTools文档。
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
|
21小时前
|
安全 调度 Swift
【Swift开发专栏】Swift中的多线程与并发编程
【4月更文挑战第30天】本文探讨Swift中的多线程与并发编程,分为三个部分:基本概念、并发编程模型和最佳实践。介绍了线程、进程、并发与并行、同步与异步的区别。Swift的并发模型包括GCD、OperationQueue及新引入的结构体Task和Actor。编写高效并发代码需注意任务粒度、避免死锁、使用线程安全集合等。Swift 5.5的并发模型简化了异步编程。理解并掌握这些知识能帮助开发者编写高效、安全的并发代码。
|
21小时前
|
安全 Java 开发者
构建高效微服务架构:后端开发的新范式Java中的多线程并发编程实践
【4月更文挑战第29天】在数字化转型的浪潮中,微服务架构已成为软件开发的一大趋势。它通过解耦复杂系统、提升可伸缩性和促进敏捷开发来满足现代企业不断变化的业务需求。本文将深入探讨微服务的核心概念、设计原则以及如何利用最新的后端技术栈构建和部署高效的微服务架构。我们将分析微服务带来的挑战,包括服务治理、数据一致性和网络延迟问题,并讨论相应的解决方案。通过实际案例分析和最佳实践的分享,旨在为后端开发者提供一套实施微服务的全面指导。 【4月更文挑战第29天】在现代软件开发中,多线程技术是提高程序性能和响应能力的重要手段。本文通过介绍Java语言的多线程机制,探讨了如何有效地实现线程同步和通信,以及如
|
21小时前
|
并行计算 数据处理 开发者
Python并发编程:解析异步IO与多线程
本文探讨了Python中的并发编程技术,着重比较了异步IO和多线程两种常见的并发模型。通过详细分析它们的特点、优劣势以及适用场景,帮助读者更好地理解并选择适合自己项目需求的并发编程方式。