并发编程 · 基础篇 · android线程那些事

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

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

并发编程 · 基础篇(下) · android线程池那些事

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

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

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

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

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

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

一、序言

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

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

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

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

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

alt

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

二、5W2H分析并发

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

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

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

alt

2.1 并发是什么?

传送门: Java Concurrency

首先我们聊一聊5W2H的WhatJava Concurrency—并发。

alt

并发、并行和串行

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

alt

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

alt

并发是指一个或若干个 CPU 对多个进程或线程之间进行多路复用。简单说线程A先做Task工作一段时间线程B再做Task

线程B执工作一段时间线程C再做Task线程C工作一段时间线程A重新执行Task。

以此类推直到工作完成看上去像是三个线程同时一起执行但其实完全可以交给一个线程执行。

对于并发来说线程A先执行一段时间然后线程B再执行一段时间接着线程C再执行一段时间。每个线程都轮流得到 CPU 的执行时间并发只需要一个 CPU 即能够实现 线程利用率最优。

并行则是指多个进程或线程同一时刻被执行是真正意义上同时执行并行必须要有多个 CPU 支持。

并行是A、B和C三个线程同时执行一个或多个Task每线程负责一项TaskA、B和C三线程在同一时刻齐头并进地完成这些事情。

并行比串行和并发时间开销要小但是由于线程A、线程B和线程C是同时执行的需要三个 CPU 才能实现一定程度影响机器性能。

用一句话总结就是:

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

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

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

alt

同步和异步

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

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

alt

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

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

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

阻塞和非阻塞

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

alt

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

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

alt

同步阻塞

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

alt

输出结果:

小木箱成长营

同步非阻塞

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

alt

输出结果:

小木箱正在学习并发编程

小木箱正在学习设计模式

异步阻塞

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

alt

输出结果:

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

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

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

异步非阻塞

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

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

alt

输出结果:

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定义

首先我们聊聊第一种方式HandlerThreadandroid的HandlerThread是一种特殊的ThreadHandlerThread提供了一个Looper可以用来处理消息和处理程序。

HandlerThread优势在于可以将耗时的任务分发到后台线程从而避免UI线程的阻塞。

HandlerThread可以让开发者将耗时的操作放到后台线程从而提高应用程序的性能和流畅性。

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的运行。
alt

因为HandlerThread可以使用Handler来发送和处理消息。所以HandlerThread可以实现多线程。

因为HandlerThread可以创建多个Handler每个Handler可以拥有自己的线程。所以HandlerThread可以实现多线程。

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

HandlerThread实现方式

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

alt

2.4.2 AsyncTask

然后我们聊聊第二种方式AsyncTaskandroid的AsyncTask通常用于执行一些短暂的耗时操作比如从网络获取数据在UI线程中执行简单的计算或者更新UI等。

AsyncTask定义

android的AsyncTask是android提供的用于实现多线程的类AsyncTask可以实现多线程协作异步执行后台任务并且可以通过主线程更新UI。

AsyncTask底层原理

android的AsyncTask实现多线程底层原理是AsyncTask创建一个新的工作线程在工作线程中调用doInBackground方法执行后台任务同时在主线程中调用onProgressUpdate方法更新UI界面。

当doInBackground执行完毕后会回调onPostExecute方法在onPostExecute方法中可以更新UI界面。

alt

AsyncTask使用方式

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

alt

2.4.3 IntentService

IntentService定义

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

alt

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

IntentService底层原理

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

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

IntentService使用方式

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

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

alt

  • 创建IntentService

alt

  • 调用IntentService

alt

2.4.4 ExecutorService

ExecutorService定义

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

alt

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

ExecutorService底层原理

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

alt

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

alt

ExecutorService使用方式

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

alt

最后小木箱对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可以直接传给ThreadFutureTask调用get方法就可以获取到线程执行结果。

alt

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

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

继承Thread类

alt

输出结果:

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

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

实现Runnable接口

alt

输出结果:

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

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

使用Callable和Future接口
alt

输出结果:

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

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

小木箱说: task运行结果4950

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

使用Executors类
alt

输出结果:

index:2

index:0

index:1

使用线程池

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

alt

输出结果:

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

alt

3.1.2.1 start

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

alt

3.1.2.2 run

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

alt

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

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

3.1.3 线程中断

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

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

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

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

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

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

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

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

alt

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

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

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

3.1.3.2 安全中断(推荐)✔️
interrupt线程中断原理
alt

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

interrupt线程中断实现

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

alt

线程中断面试题

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

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

alt

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

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

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

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

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

3.1.4 线程切换

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

3.2 线程属性

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

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

alt

输出结果:

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

alt

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

3.2.2 线程名字

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

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

3.2.3 守护线程

alt

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

3.2.4 线程优先级

alt

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

3.3 线程通信

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

3.3.1 wait/notify/notifyAll

alt

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

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

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

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

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

alt

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

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

alt

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

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

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

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

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

alt

输出结果:

Thread[#22waitThreadA5main] wait !

Thread[#24waitThreadC5main] wait !

Thread[#23waitThreadB5main] wait !

Thread[#1main5main] notify !

Thread[#1main5main] notifyAll !

Thread[#24waitThreadC5main] wait !

通过上述代码我们可以看到 当线程A调用对象的wait方法时线程A就会放弃对象锁进入等待此对象的等待锁定池只有针对此对象调用notify方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。 当线程A调用对象的notify方法时线程A就会唤醒等待此对象的线程B线程B会进入对象锁定池准备获取对象锁进入运行状态。 当线程A调用对象的notifyAll方法时线程A就会唤醒所有等待此对象的线程这些线程都会进入对象锁定池准备获取对象锁进入运行状态。

当然wait和notify也是生产者-消费者的实现模型具体事项细节可以参考 #5.1

3.3.2 join

然后说说第二种join方法主线程需要获得子线程的执行结果join方法的主要作用是等待调用该方法的线程终止。

alt

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

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

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

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

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

alt

输出结果:

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

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

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

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

alt

输出结果:

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

小木箱开发新需求功能

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

3.3.3 await/singal/singalAll

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

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

alt

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

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

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

alt

输出结果:

ABC

ABC

ABC

3.3.4 CountDownLatch

alt

最后说说第三种CountDownLatchCountDownLatch底层原理是利用可重入锁ReentrantLock和条件变量Condition同时还有一个计数器count。

当count的值大于0时表示还有任务没有完成await方法会被阻塞当count的值等于0时表示所有任务已经完成await方法会返回。

countDown方法会将count减1当count减至0时会唤醒await方法返回。

CountDownLatch适用场景是用来进行多个线程的同步管理线程调用了countDownLatch.await 之后需要等待countDownLatch的信号countDownLatch.countDown 在收到信号前CountDownLatch不会往下执行。 下面小木箱用代码实现三个线程依次打印ABC

alt

输出结果:

小木箱成长营A

小木箱成长营B

小木箱成长营C

3.4 线程运行状态

线程运行状态图

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

alt
alt

线程运行状态

  • Time waiting(睡眠)

    • Thread.sleep
  • Waiting(等待)

    • 定义

      • 等待其他thread显式的唤醒否则不会被分配CPU时间进入方法
    • 形式

      • object.wait
      • Thread.join
      • LockSupport.park
  • Blocked(阻塞)

    • 等待获取一个排它锁如果其他thread释放了lock就会结束此状态

线程挂起/恢复

挂起线程是指把正在运行的线程暂停线程暂停后线程处于阻塞状态不会消耗CPU资源但是线程的状态仍然是RUNNABLE只是没有被调度到CPU上执行。

恢复线程是指把挂起的线程重新调度到CPU上执行线程恢复后线程处于就绪状态可以被调度到CPU上执行消耗CPU资源。

线程挂起/恢复方法有: join与sleepwait与notify两组方法。

首先我们来说一下join与sleepjoin线程是指用线程对象调用如果在一个线程A中调用另一个线程B的join方法那么线程A将会等待线程B执行完毕后再执行。

alt

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

alt

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

alt
alt

3.5 生产者消费者模型

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

alt

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

3.5.1 wait/notify

alt

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

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

alt

输出结果:

小燕子 --> 女

小木箱 --> 男

小燕子 --> 女

小木箱 --> 男

3.5.2 ReentrantLock

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

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

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

alt

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

高级功能1: 等待可中断

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

alt

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

alt
高级功能2: 公平锁

alt

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

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

ReentrantLock非公平锁

alt

ReentrantLock公平锁

alt
高级功能3: ReentrantLock + Condition
alt

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

alt

3.5.4 BlockingQueue

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

alt

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

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

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

alt

3.5.5 Semaphore

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

alt

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

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

Semaphore提供了PProberen和VVerhogen两种操作P操作使计数器减1V操作使计数器加1。

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

alt

3.5.6 PipedInputStream / PipedOutputStream

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

alt

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

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

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

alt

四、线程安全

说完线程基础我们聊一聊线程安全线程安全首先有六个问题需要大家一起思考

alt

4.1 带着问题出发

4.1.1 什么是线程安全?

第一个问题是什么是线程安全?

《Java Concurrency In Practice》的作者Brian Goetz说过当多个线程访问一个对象时如果不用考虑这些线程在运行时环境下的调度和交替执行也不需要进行额外的同步或者在调用方进行任何其他的协调操作调用这个对象的行为都可以获得正确的结果那这个对象是线程安全的

通俗一点来说: 线程安全是指多线程访问同一个资源时不会因为线程交叉执行而导致资源混乱从而保证程序的正确性。

4.1.2 你知道有哪些线程不安全的情况

第二个问题是你知道有哪些线程不安全的情况

小木箱从android、容器和线程池三个方面举例说明线程不安全的情况。

首先在android多线程编程中如果多个线程同时访问同一个Activity里的资源也可能导致线程不安全的情况因为Activity是一个单例它的资源可能被多个线程同时访问。

alt

上面的代码中MyActivity类中定义了一个counter变量并且在onCreate方法中启动了两个线程两个线程都会更新counter变量但是由于没有进行同步操作这两个线程可能会同时访问counter变量从而导致线程不安全的问题从而可能引发Java异常。

为了解决这个问题可以在更新counter变量时使用同步操作例如使用synchronized关键字

alt

然后在android UI操作并不是线程安全的并且这些操作必须在UI线程执行子线程是无法更新UI的具体实现思路如下:

alt

但这并非绝对的子线程其实也是可以更新UI的

Toast本质是通过window显示和绘制的而子线程不能更新UI在于ViewRootImpl的checkThread方法无法在Activity维护View树的行为。

然后HashMap的putVal方法添加元素不是线程安全的因此可通过Collections类的静态方法synchronizedMap获得线程安全的HashMap

alt

最后在ThreadPoolExecutor中addWorker为什么需要持有mainLock本质原因是workers是HashSet类型的不能保证线程安全。

alt
4.1.3 怎么避免线程安全问题?

第三个问题怎么避免线程安全问题?当存在多个线程协作共享数据时需要保证同一时刻有且只有一个线程在操作共享数据其他线程必须等到该线程处理完数据后再进行保证线程安全的方式有三种。

第一种是使用线程安全的类如AtomicInteger类

第二种是加锁排队执行如synchronized和ReentrantLock等使用

第三种是使用线程本地变量如ThreadLocal来处理。

后文会详细讲解实现过程和原理。

4.1.4 多线程会带来哪些线程安全问题?

多线程会带来哪些线程安全问题?多线程会带来四个线程安全问题。第一个问题是原子性问题第二个问题是竞争条件第三个问题是死锁第四个问题是丢失更新。

首先我们来说说第一个问题原子性问题某些操作不能被中断比如赋值操作如果多线程同时对同一变量进行赋值操作会导致变量的值不正确。比如多线程同时访问 i++ 的场景

alt

如下图异常代码所示输出结果可能小于20因为多线程同时对count变量进行赋值操作可能会出现线程安全问题。为了解决这个问题可以使用同步代码块保证变量的操作是原子性的

alt

输出结果:

18

那么正确的编码是怎样的呢我们应该使用同步代码块可以保证count变量的操作是原子性的

alt

输出结果:

20

第二个问题是竞争条件当多个线程同时访问某个变量时某个线程修改了变量的值但是其他线程没有读取到修改后的值导致程序出现错误如下面的代码所示

alt

输出结果:

Thread-1 : 2

Thread-3 : 4

Thread-0 : 1

Thread-4 : 5

Thread-2 : 3

上述代码中5个线程同时访问count变量并且在run方法中将count变量加1但是由于多线程的存在有可能某个线程修改了count变量的值而其他线程还没有读取到修改后的值这就可能导致程序出现错误。

为了解决这个问题可以使用synchronized关键字来对count变量进行同步操作

alt

输出结果:

Thread-0:1

Thread-4:2

Thread-3:3

Thread-2:4

Thread-1:5

第三个问题是死锁多个线程互相等待对方释放某个资源导致程序无法继续执行。

假设有两个小木箱线程P1和小木箱线程P2它们分别需要资源A和资源B。

但是它们同时只能获得一个资源当小木箱线程P1获得资源A时P2获得资源B然后小木箱线程P1等待资源B小木箱P2等待资源A。

但是由于资源A和资源B只有一个所以小木箱线程P1和小木箱线程P2都无法获得自己想要的资源这就是死锁。

死锁的原因是由于小木箱线程P1和小木箱线程P2同时请求资源A和资源B而资源A和资源B只有一个。

所以小木箱线程P1和小木箱线程P2都无法获得自己想要的资源从而导致死锁的发生。

alt

输出结果:

Thread[#23小木箱线程P25main]get ResB

Thread[#22小木箱线程P15main]get ResA

Thread[#23小木箱线程P25main]waiting get ResA

Thread[#22小木箱线程P15main]waiting get ResB

最后一个问题是丢失更新。

丢失更新是指当多个线程同时访问某个变量时某个线程修改了变量的值。

但是其他线程没有读取到修改后的值导致程序出现错误。

主要原因是多个线程同时访问某个变量而该变量没有被同步导致其他线程读取到的值不是最新的值。

alt

输出结果:

count=10000

解决方案有三种第一种是使用synchronized关键字使用synchronized关键字可以解决多线程访问变量时出现的线程安全问题。第二种是使用Atomic类Atomic类提供了一种原子操作可以解决多线程访问变量时出现的线程安全问题。第三种是使用volatile关键字volatile关键字可以保证多线程访问变量时可以读取到最新的值。

4.1.5 线程安全问题体现是怎样的?

线程编程会带来性能问题呢主要有两个方面第一个方面是线程调度第二个方面是线程协作。

首先我们说说第一个方面线程调度具体体现是缓存失效带来性能问题。下面代码存在线程不安全的问题因为多个线程同时对SUM变量进行操作可能会出现脏读、脏写等问题。

alt

输出结果:

199269

可以使用synchronized关键字加锁来解决。

alt

输出结果:

200000

由于程序有很大概率会再次访问刚才访问过的数据所以为了加速整个程序的运行会使用缓存这样我们在使用相同数据时就可以很快地获取数据。

可一旦进行了线程调度切换到其他线程CPU就会去执行不同的代码因为CPU读写缓存速度低于内存读写缓存速度原有缓存很可能失效需要重新读写缓存新数据造成一定的开销。

因此线程调度器为了避免频繁地发生上下文切换有四种解决方式分别是减少线程的数量、 调整线程优先级、 整线程优先级、调整时间片大小和使用预取策略。

减少线程的数量方面我们可以减少线程的数量可以减少上下文切换的发生次数这样可以提高效率。

调整线程优先级方面我们在线程调度器中可以通过调整线程的优先级来控制上下文切换次数使计算机系统能够更高效地运行。

调整时间片大小方面我们在线程调度器可以通过调整时间片大小来控制上下文切换的发生次数以提高系统效率。

使用预取策略方面我们在线程调度器可以使用预取策略来减少上下文切换次数以提高系统效率。

首先我们说说第二个方面线程协作导致线程不安全的情况。

因为主线程在设置running变量为false之前另一个线程可能已经读取了running变量的值并将它设置为true。这样循环就会一直运行导致线程不安全的情况。

alt

为了解决这个问题可以使用synchronized关键字来确保变量running的原子操作

alt

因为线程之间如果有共享数据为了避免数据错乱为了保证线程安全可能禁止编译器和 CPU 对其进行重排序等优化也可能出于同步的目的反复把线程工作内存的数据 flush 到主存中然后再从主内存 refresh 到其他线程的工作内存中。

这些问题在单线程中并不存在但在多线程中为了确保数据的正确性就不得不采取上述方法因为线程安全的优先级要比性能优先级更高间接降低了性能。

4.1.6 怎样获取子线程的结果?

第五个问题为怎样获取子线程的结果?获取子线程的结果有两种方式第一种方式是FutureCallable第二种方式是Callable接口。

因为Runnable没有具体返回值也不能抛出checked Exception。

alt

如果我们想要拿到线程执行结果那么建议使用Future和Callable方式。

Future是一个存储器Future存储了call0这个任务的结果而这个任务的执行时间是无法提前确定的。

因为这完全取决于call方法执行的情况通过Future.get来获Callable取接口返回的执行结果。

alt

输出结果:

alt

如果我们想取消任务的执行我们可以调用cancel方法。

  1. 如果这个任务还没有开始执行那么这种情况最简单任务会被正常的取消未来也不会被执行方法返回true。
  2. 如果任务已完成或者已取消那么cancel方法会执行失败方法返回false。
  3. 如果这个任务已经开始执行了那么这个取消方法将不会直接取消该任务而是会根据我们填的参数。 mayInterruptIfRunning。

使用Future的注意点有两个。

第一个是当for循环批future量获取的结果时容易发生一部分线程很慢的情况get方法调用timeout时应使用限制。

第二个是生命周期只能前进不能后退。就和线程池的生命周期一样一旦完全完成了任务Future就永久停在了“已完成”的 状态不能重头再来。


Callable比较简单了类似于Runnable被其它线程执行的任务实现call方法有返回值

alt

输出结果:

小木箱说: 我是call的返回值

4.1.7 什么是多线程的上下文切换

第六个问题什么是多线程的上下文切换在实际开发中线程数往往是大于 CPU 核心数的比如 CPU 核心数可能是 8 核、16 核等等但线程数可能达到成百上千个。

这种情况下操作系统就会按照一定的调度算法给每个线程分配时间片让每个线程都有机会得到运行。

而在进行调度时就会引起上下文切换上下文切换会挂起当前正在执行的线程并保存当前的状态然后寻找下一处即将恢复执行的代码唤醒下一个线程以此类推反复执行。

但上下文切换带来的开销是比较大的假设我们的任务内容非常短比如只进行简单的计算那么就有可能发生我们上下文切换带来的性能开销比执行线程本身内容带来的开销还要大的情况。

那么什么情况会导致密集的上下文切换呢如果程序频繁地竞争锁或者由于 IO 读写等原因导致频繁阻塞那么程序就可能需要更多的上下文切换上下文切换导致了更大的开销我们应该尽量避免这种情况的发生。

4.2 线程安全特性

带着问题出发小木箱说完了接下来我们聊一下线程安全特性无论是缓存失效、还是上下文切换带来的时序性问题和线程调度引发的数据准确性问题。

在深入理解Java虚拟机那本书统一归纳总结为线程安全的三大特性: 原子性、有序性和可见性。

alt

4.2.1 可见性

当线程 A在CPU1上执行线程 B在CPU2上执行共享变量param=0线程 A给共享变量param赋值时会把param的初始值加载到CPU1的高速cache中然后赋值2线程 B给共享变量param赋值时会把param的初始值加载到CPU2的高速cache中。此时param的值在CPU2中是0而不是2。线程 A在CPU1中修改了param但是CPU2中的线程 B却没有拿到。

保证线程可见性的方式有三种第一种是提供volatile关键字保证可见性。

当一个变量被volatile修饰时保证修改的值会被立刻更新到主内存中当其他线程读取时会去主内存中读取新值。

第二种是synchronized。第三种是Lock。

lock和synchronized因为同一时间只有一个线程执行锁释放之前会把变量的修改刷新到主内存中.

alt

使用volatile关键字可以确保变量可见性。当一个线程对volatile变量进行写操作时会导致其他线程对该变量的读操作立即可见

alt

使用synchronized关键字可以确保变量可见性。当一个线程访问一个对象的synchronized方法或者synchronized块时其他线程对该对象的其他synchronized方法或者synchronized块的访问将被阻塞直到访问线程离开synchronized方法或者synchronized块时其他线程才可以访问该对象的其他synchronized方法或者synchronized块。

alt

这样一来synchronized关键字可以保证在同一时刻只有一个线程可以执行某个方法或者某个代码块从而也就保证了操作的可见性即一个线程修改了某个共享变量的值这新值对其他线程来说是可见的。

ReentrantLock通过使用内部的可重入锁来保证可见性。当线程获得锁时它会同步内存确保所有线程都能看到最新的状态。当线程释放锁时ReentrantLock会将最新的状态写回主内存确保其他线程能够看到最新的状态。

alt

那么保证线程可见性的本质是什么呢? Happens-Before原则 什么是Happens-Before原则呢? JMM向程序员提供了足够强内存可见性保证不影响程序执行结果情况可见性保证并一定存在比如下面的程序A Happens-Before B 并不保证因为其不影响程序执行结果

alt

JMM为了满足编译器和处理器的约束尽可能少

alt

Happens-Before遵循的规则是只要不改变程序的执行结果编译器和处理器想怎么优化就怎么优化。

Happens-Before核心思想是: 前一个操作的结果对后续操作时可见的

Happens-Before目的是: 为了在不改变程序执行结果的前提下尽可能地提高程序执行的并行度。

4.2.2 有序性

JMM (Java Memory Model) 有序性问题是指在多线程编程中由于编译器和处理器优化导致程序执行顺序与代码顺序不一致的问题。JMM 为程序员提供了一种可靠的机制来确保程序的正确性从而避免出现不可预料的结果。

alt

因为JMM定义了一种内存模型该模型定义了线程之间的内存访问顺序但是由于处理器的指令重排序导致线程之间的内存访问顺序可能会发生改变。

alt

这就导致了线程之间存在有序性问题从而出现了程序的不一致性。

alt

JMM有序性问题可以通过使用使用synchronized关键字、使用Lock锁和使用Atomic类来解决 使用synchronized关键字可以确保每次对变量的读写操作都是从主内存中读取最新的值从而保证线程间变量的一致性。

alt

当一个线程访问一个对象的synchronized代码块时其他线程便不能访问该对象的其他synchronized代码块。

Lock锁可以确保每次对变量的读写操作都是从主内存中读取最新的值从而保证线程间变量的一致性。当多个线程访问同一个ReentrantLock对象时多个线程会按照获取锁的顺序依次获取锁而不会发生竞争从而保证线程安全。

alt

ReentrantLock使用一个可重入的锁来确保线程按照顺序访问共享资源。ReentrantLock使用FIFO先进先出的锁队列来管理等待的线程从而保证线程按照它们发出请求的顺序来访问共享资源。

Atomic类提供了一种可以实现原子操作的方法从而保证线程间变量的一致性。Atomic还可以通过使用内存屏障来保证操作的有序性内存屏障可以确保操作在特定时间点完成从而保证操作的有序性。

alt

在实际开发过程中Android中存在多个进程多个进程间通过IPC进行数据交互如果没有同步机制会出现JMM有序性问题。

4.2.3 原子性

JVM原子性是指Java虚拟机中的指令是原子的也就是说它们不能被其他线程中断或改变。这意味着当一个线程正在执行某个指令时其他线程就不能改变它。这确保了程序的正确性避免了线程之间的竞争条件。

alt

JVM通过使用synchronized关键字、使用Atomic类、使用Lock锁和使用CAS算法来确保原子性。

使用Synchronized关键字可以保证在同一时刻只有一个线程可以执行某个方法或某个代码块Synchronized可以保证操作的原子性即操作过程不可被中断Synchronized使用的是互斥锁机制能够保证同一时刻只有一个线程可以访问某个资源从而保证了操作的原子性。

alt

使用Atomic类的方法可以保证操作的原子性即每次操作都是不可中断的也就是说在一个线程进行操作的过程中其他线程不能中断或插入只有当前线程完成操作后其他线程才能进行操作。

alt

使用ReentrantLock可以保证原子性因为ReentrantLock使用了一个可重入的锁ReentrantLock可以保证操作的原子性即一个操作必须在另一个操作完成之前完成。ReentrantLock还可以通过使用比较和交换CAS技术来确保原子性从而保证操作的一致性和正确性。

alt

使用CAS算法可以通过比较并交换操作来确保原子性。当多个线程同时尝试修改某个变量时CAS算法会检查变量当前值是否与预期值相等如果相等就修改变量的值否则就不做任何修改。这种方式可以确保多个线程同时修改变量时只有一个线程的修改能够成功从而保证了原子性。

alt

简单总结一下:

如果多线程访问同一块资源时候你想要保证资源的可见性那么小木箱建议你使用volatile、synchronized、ReentrantLock和Atomic

如果多线程访问同一块资源时候你想要保证资源的有序性那么小木箱建议你使用synchronized和ReentrantLock

如果多线程访问同一块资源时候你想要保证资源的原子性那么小木箱建议你使用volatile、synchronized、Atomic、ReentrantLock和CAS算法

4.3 线程安全强度

线程安全特性小木箱说完了接下来我们聊一下线程安全强度线程安全强度有三大特征: 第一线程不可变。第二绝对线程安全。第三相对线程安全。第四线程隔离。

线程不可变

线程不可变是指一个线程实例的状态在它被创建之后不能被改变的概念。也就是说一旦一个线程被创建它的属性例如它的优先级名字等都不能被改变。

不可变的object一定是线程安全的因为线程的状态是由操作系统内核控制的操作系统内核不允许线程的状态发生变化。

使用final基础数据类型、string、枚举类型、Number部分子类和集合类型Collections.unmodifiableXXX()获取不可变集合都可以保证线程不可变

绝对线程安全

绝对线程安全是指在多线程环境下任何时刻都不会出现数据不一致的情况也就是说不管多少个线程同时对同一个数据进行操作最终结果都是一致的一个线程对数据的改变其他线程都能看到也就是说绝对线程安全是指多个线程同时对数据进行操作时数据的一致性是绝对保证的。

绝对线程安全的实现可以降低系统出现线程安全问题的可能性提高系统的稳定性和可靠性。

不管运行环境如何调用者都不需要任何额外的sync操作比如ATM取钱怎么去的和取完之后怎么拿走不影响取钱这个业务安全

相对线程安全

相对线程安全是指在多线程环境下有一定的约束条件下不会出现数据不一致的情况也就是说在满足一定的条件下多个线程同时对数据进行操作时数据的一致性是相对保证的。

保证对这个object单独的操作是thread安全的但是对一些特定顺序的连续调用需要额外的手段来保证java中的大部分thread安全类都属于这种类型比如Vector、HashTable、Collections、synchronizedCollection()方法包装的集合

线程隔离

线程隔离指对象本身不是thread安全的但是可以在调用端采用正确的同步手段来保证对象在并发环境中安全的使用。

为了提高系统的并发性能减少线程之间的竞争。 为了提高系统的安全性防止一个线程对其他线程造成损害。为了提高系统的稳定性防止一个线程中断其他线程的执行。为了提高系统的可维护性防止一个线程影响其他线程的运行。JVM对各个线程进行独立隔离

线程对立

无法在多线程环境中并发使用的代码java中几乎没有因为会导致一种不可控制的状态从而使系统处于不稳定的状态。

线程对立的目的是为了保护多个线程之间的共享资源避免不同线程之间的数据竞争从而防止程序出现数据不一致的问题。

4.4 线程安全方案

线程安全强度小木箱说完了接下来我们聊一下线程安全方案小木箱将上述的安全策略归纳总结为四大类互斥阻塞同步、非阻塞同步、无同步方案和控制并发流程

4.4.1 互斥阻塞同步

阻塞线程执行达到同步的目的

synchronized

按照锁的使用位置synchronized锁的类型有四种: 第一种是对象锁第二种是方法锁第三种是类锁第四种是静态方法锁

第一种是对象锁synchronized(this)同步一个代码块只作用于一个对象如果调用两个对象的同步代码块方法不会同步可以用来保护一个对象的实例方法使得多个线程可以同时访问但是同一时刻只能有一个线程可以执行该方法以避免多线程环境中数据出现错误。

第二种是类锁synchronized(Student.class)同步一个类对这个类的所有对象同步可以用来保护一个类的静态方法使得多个线程可以同时访问但是同一时刻只能有一个线程可以执行该方法以避免多线程环境中数据出现错误。

第三种是方法锁synchronized method同步一个方法作用同上只作用于一个对象当多个线程访问同一个对象的实例方法时它们会被同步以保证线程安全。

第四种是静态方法锁synchronized static method同步静态方法作用同步一个类当多个线程访问同一个类的静态方法时它们会被同步以保证线程安全。 alt   新版本jvm对synchronized进行了很多优化例如自旋锁等synchronized的性能略微比ReentrantLock差一点

独占锁Reentrantlock、共享锁CountDownLatch、CyclicBarrier、Phaser、Semaphore等
4.4.2 非阻塞同步

非阻塞同步主要解决线程等待、切换带来的性能问题。基于冲突检测的乐观并发策略先操作如果没有其他线程竞争就直接成功否则采取补偿措施不断重试直到成功底层需要硬件指令集支持。

CAS(compare and swap) 和原子类AtomicInteger的方法compareAndsSet()、getAndIncrement()都是非阻塞同步表现形式

4.4.3 无同步方案

所谓的无同步方案就是控制并发流程

什么是控制并发流程?

控制并发流程是指保证线程安全不一定要同步如果一个method不涉及共享数据就无须同步。

控制并发流程特征
  • 特征一: 栈封闭

多个thread访问同一个method的局部变量时不会出现thread安全问题因为局部变量存储在虚拟机栈中属于thread私有的

  • 特征二: 线程本地存储(Thread Local Storage)

如果一段代码中的数据必须与其他线程共享那就保证共享数据代码在同一个thread里面执行无须同步也能保证thread不会出现数据争用的问题

可以使用ThreadLocal类实现thread本地存储功能

  • 特征三: 可重入代码

可以在代码执行的任何时刻中断转而执行另外一段代码原来的程序不会出现任何错误

怎样控制并发流程

Java控制并发可以使用Cyclicbarrier和RxJava来控制并发流程。下面着重来讲解CyclicbarrierCyclicbarrier是一种同步工具类Cyclicbarrier允许一组线程相互等待直到到达某个公共屏障点common barrier point。

alt

当所有线程都到达屏障点时屏障点才会打开所有线程才能继续执行。 Cyclicbarrier可以用来实现多线程之间的协作比如说玩家在游戏中到达某个关卡时所有玩家都要到达某个位置然后才能继续游戏这时候就可以使用Cyclicbarrier了。 Cyclicbarrier底层使用AQSAbstractQueuedSynchronizer实现AQS是一种基于FIFO队列实现的锁机制AQS通过一个int变量来表示同步状态当状态为0时表示无锁状态当状态大于0时表示有锁状态。

Cyclicbarrier在内部维护了一个计数器每当一个线程到达屏障点时计数器的值就会减1当计数器的值减为0时表示所有线程都已经到达屏障点此时就会打开屏障所有线程继续执行。

alt

Cyclicbarrier优点有两个第一个是Cyclicbarrier可以实现让一组线程等待至某个状态之后再全部同时执行。 第二个是Cyclicbarrier可以复用当线程到达屏障点后计数器会重置为初始值这样就可以多次使用Cyclicbarrier了。

Cyclicbarrier缺点有两个第一个是当某个线程超时或者被中断时整个系统都会受到影响因为其他线程都会被阻塞。第二个是如果线程太多可能会导致计数器溢出。

简单总结一下就是:

如果你想保证线程不可变那么小木箱建议你使用String、Integer 、volatile和 ConcurrentHashMap

如果你想保证线程相对安全那么小木箱建议你使用mutex、semaphore、lock、局部变量、可重入函数、非阻塞算法和Vector等

如果你想保证线程处于隔离状态那么小木箱建议你在Linux使用Namespace机制、使用锁和ThreadLocal

如果你想确保绝对的线程对立那么小木箱建议你使用原子操作、线程池访问权限管控、同步锁、volatile关键字和信号量

如果你想确保绝对的线程安全那么可以使用原子操作、同步锁、volatile关键字和ConcurrentHashMap

4.5 UncaughtException兜底

最后我们尝试回答一个问题: 线程的未捕获异常UncaughtException应该如何处理

alt

当线程抛UncaughtException我们可以利用UncaughtExceptionHandler处理因为主线程可以轻松发现异常子线程却不行。

在子线程抛出了异常会被主线程覆盖子线程异常无法用传统方法捕获子线程抛出异常。

主线程try catch没用只能捕获当前线程的异常不能直接捕获所有异常因此UncaughtExceptionHandler提高了代码健壮性。

alt

输出结果: 小木箱成长营捕获了Thread-0线程的异常

这样我们可以全局为不健康的线程进行兜底管控。

五、结语

并发编程 · 基础篇(上) · android线程那些事课程就此结束了对于每一个Android开发者来说线程知识重要性不言而喻国内为什么老八股喜欢考线程知识因为如果你不具备这方面扎实的线程安全和线程基础知识那么应对高性能下载组件实现还是处理启动和卡顿优化等工作都非常棘手。

下一节小木箱将带大家学习并发编程 · 基础篇(下) · android线程池那些事。

我是小木箱如果大家对我的文章感兴趣那么欢迎关注小木箱的公众号小木箱成长营。小木箱成长营一个专注移动端分享的互联网成长社区。

本文由 mdnice 多平台发布

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: android