做房产必知的发布房源网站,哈尔滨做网站搭建的,在centos上做网站,中核集团为什么开发中需要并发编程#xff1f;
加快响应用户的时间使你的代码模块化、异步化、简单化充分利用CPU资源
基础概念
进程和线程
进程 我们常听说的应用程序#xff0c;由指令和数据组成。当我们不运行应用程序时#xff0c;这些应用程序就是放在磁盘上的二进制的代码…为什么开发中需要并发编程
加快响应用户的时间使你的代码模块化、异步化、简单化充分利用CPU资源
基础概念
进程和线程
进程 我们常听说的应用程序由指令和数据组成。当我们不运行应用程序时这些应用程序就是放在磁盘上的二进制的代码。一旦我们运行这些应用程序指令要运行数据要读写就必须将指令加载至CPU数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备从这种角度来说进程就是用来加载指令、管理内存、管理IO的。 站在操作系统的角度进程是程序运行资源分配(以内存为主)的最小单位。
线程 线程是CPU调度的最小单位线程必须依赖于进程而存在线程是进程中的一个实体是CPU调度和分派的基本单位它是比进程更小的、能独立运行的基本单位。线程自己基本上不拥有系统资源只拥有在运行中必不可少的资源(如程序计数器、一组寄存器和栈)但是它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个进程可以拥有多个线程一个线程必须有一个父进程。 同一台计算机的进程通信称为IPC(Inter-process communication)不同计算机之间的通信称为RPC需要通过网络并遵守共同的协议比如大家熟悉的Dubbo就是一个PRC框架而Http协议也经常用在PRC上比如SpringCloud微服务。 进程间通信有几种方式
管道分为匿名管道(pipe)及命名管道(named pipe)匿名管道可用于可用于具有亲缘关系的父子进程之间的通信命名管道除了具有管道所具有的功能外还允许无亲缘关系进程间的通信。信号(signal)信号是在软件层次上对中断机制的一种模拟它是比较复杂的通信方式用于通知进程有某事件发生一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。消息队列(message queue)消息队列是消息的链接表它克服了上两种通信方式中信号量有限的缺点具有写权限的进程可以按照一定的规则向消息队列中添加新消息对消息队列有读权限的进程则可以从消息队列读取信息。共享内存(shared memory)它使得多个进程可以访问同一块内存区域不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作如互斥锁和信号量等。信号量(semaphore)主要作为进程间及同一进程的不同线程之间的同步和互斥手段。套接字(socket)这是一种更为一般的进程间通信机制它可用于网络中不同机器之间的进程间通信应用非常广泛。同一机器中的进程还可以使用Unix domain socket(比如同一机器中MySQL中的控制台mysql shell和MySQL服务程序的连接)这种方式不需要经过网络协议栈不需要打包拆包、计算校验和维护序号和应答等比纯粹基于网络的进程间通信肯定效率更高。
CPU核心数和线程数的关系 目前主流CPU都是多核的线程是CPU调度的最小单位。同一时刻一个CPU核心职能运行一个线程也就是CPU和线程数的关系是1:1的关系也就是说8核处理器可以同时执行8个线程的代码。但Intel引入超线程技术后使核心数与线程数形成1:2的关系。 在Java中提供了Runtime.getRuntime().availableProcessors()可以让我们获取当前的CPU核心数注意这个核心数是逻辑处理器数。
上下文切换 上下文是CPU寄存器和程序计数器在任何时间点的内容。 寄存器是CPU内部的一小部分非常快的内存它通过提供对常用值的快速访问来加快计算机程序的执行。 程序计数器是一种专门的寄存器它指示CPU在其指令序列中的位置并保存着正在执行的指令地址或下一条要执行的指令的地址这取决于具体的系统。 上下文切换可以更详细地描述为内核对CPU上的进程(包括线程)执行以下活动
暂停一个进程的处理并将该进程的CPU状态(即上下文)存储在内存的某个地方从内存中获取下一个进程的上下文并在CPU的寄存器中恢复它返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程 引发上下文切换的原因一般包括线程、进程切换、系统调用等等就CPU时间而言一次上下文切换大概需要500020000个时钟周期相对一个简单指令几个乃至十几个左右的执行时钟周期成本是巨大的。
并行与并发 并发Concurrent指应用能够交替执行不同的任务比如单CPU核心下执行多线程并非同时执行多个任务如果你打开两个线程这两个线程会交替执行。当谈论并发的时候一定要加个单位时间也就是说单位时间内并发量是多少离开了单位时间其实是没有意义的。 并行Parallel指应用能够同时执行不同的任务。 两者之间的区别一个是交替执行一个是同时执行。
Java中的多线程 一个Java程序的运行就算用户没有自己 开启线程实际也有很多JVM自行启动的线程一般来说有 [6] Monitor Ctrl-Break//监控Ctrl-Break中断信号 [5] Attach Listener//内存dump、线程dump、类信息统计、获取系统属性等 [4] Signal Dispatcher//分发处理发送给JVM信号的线程 [3] Finalizer//调用对象finalize方法的线程 [2] Reference Handler//清除Referene的线程 [1] main//用户程序入口
线程的启动与中止
启动
启动的方式有
继承Thread
public class ThreadTest {public static void main() {Thread thread new Thread() {public void run() {System.out.println(test);}}thread.start();//开始线程}
}实现Runnable接口
public class ThreadTest {public static void main() {Runnable runnable new Runnable() {System.out.println(Runnable);}Thread thread new Thread(runnable);thread.start();//开始线程}
}Thread和Runnable的区别 Thread是Java里对线程的唯一抽象Runnable只是对任务(业务逻辑)的抽象。Thread可以接受任意一个Runnable的实例并执行
Callable、Future和FutureTask Runnable是一个接口其中声明了run()方法由于run()方法无返回值所以在执行完任务之后无法返回任何结果。 Callable位于java.util.concurrent包下它也是一个接口在它里面声明了一个方法call()这是一个泛型接口call()函数返回的类型就是传递进来的V类型
FunctionalInterface
public interface CallableV {/*** Computes a result, or throws an exception if unable to do so.** return computed result* throws Exception if unable to compute a result*/V call() throws Exception;
}Future就是对具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果该方法会阻塞直到任务返回结果。 FutureTask类实现了RunnableFuture接口RunnableFuture继承了Runnable接口和Future接口而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行又可以作为Future的到Callable的返回值。 因此我们通过一个线程运行Callable但Thread不支持构造方法中传递Calable的实例所以需要通过FutureTask把一个Callable包装成Runnable然后在通过这个FutureTask拿到Callable运行后的返回值 新启线程有几种方式 官方说是两种一种是继承Thread类一种是实现Runnable接口
中止
线程自然终止要么是run执行完成了要么是跑出了一个未处理的异常导致线程提前结束。 stop暂停、恢复和停止操作对应在线程Thread地API就是suspend()、resume()、stop()。但是这些API是过期的也就是不建议使用的。不建议使用的原因主要有以suspend()方法为例在调用后线程不会释放已经占有的资源(比如锁)而是占有着资源进入睡眠状态这样容易引发死锁问题。 中断 安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作中断好比其他线程对该线程打了个招呼“A你要中断了”不代表线程A会立即停止自己的工作同样的A线程完全可以不理会这种中断请求。 线程通过方法isInterrupted()来进行判断是否被中断 也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断。 如果一个线程处于阻塞状态则在线程在检查中断标示时如果发现中断标示为true则会在这些阻塞方法调用处抛出InterruptedException异常并且在抛出异常后会立即将线程的中断标示位清除即重新设置为false。 不建议自定义一个取消标志位来中止线程的运行。因为run方法里有阻塞调用时会无法很快检测到取消标志线程必须从阻塞调用返回后才会检查这个取消标志这种情况下使用中断会更好因为
一般的阻塞方法如 sleep 等本身就支持中断的检查检查中断位的状态和检查取消标志位没什么区别 用中断位的状态还可以避免声明取消标志位减少资源的消耗。 注意处于死锁状态的线程无法被中断
深入理解run()和start() Thread类时Java里对线程概念的抽象可以这样理解我们通过new Thread()其实只是new出一个Thread的实例还没有和操作系统中真正的线程挂钩。只有执行了start()方法后才实现了真正意义上的启动线程。 start()方法让一个线程进入就绪队列等待分配 cpu分到 cpu 后才调用实现 的 run()方法start()方法不能重复调用如果重复调用会抛出异常。
深入学习Java的线程
线程的状态/生命周期 Java中的线程状态分为6种
初始(NEW)新创建了一个线程对象但还没有调用start()方法。运行(RUNNABLE)Java 线程中将就绪ready和运行中running两种 状态笼统的称为“运行”。 线程对象创建后其他线程(比如 main 线程调用了该对象的 start()方法。 该状态的线程位于可运行线程池中 等待被线程调度选中 获取 CPU 的使用权 此时处于就绪状态ready。就绪状态的线程在获得 CPU 时间片后变为运行中 状态running。阻塞(BLOCKED)表示线程阻塞于锁。等待(WAITING)进入该状态的线程需要等待其他线程做出一些特定动作 通知或中断。超时等待(TIMED_WAITING)该状态不同于 WAITING它可以在指定的时 间后自行返回。终止(TERMINATED)表示该线程已经执行完毕。 状态之间的变迁如下图所示
其他线程相关方法 yield()方法使当前线程让出CPU占有权但让出的时间是不可设定的。也不会释放锁资源同时执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。 比如ConcurrentHashMap#initTable方法中就使用了这个方法这是因为ConcurrentHashMap中可能被多个线程同时初始化table但是其实这个时候只允许一个线程进行初始化操作其他的线程就需要被阻塞或等待但是初始化操作其实很快这里Doug Lea大师为了避免阻塞或者等待这些操作引发的上下文切换等等开销就让其他不执行初始化操作的线程干脆执行yield()方法以让出CPU执行权让执行初始化操作的线程可以更快的执行完成。
线程的优先级 在Java线程中通过一个整型变量priority来控制优先级优先级的范围从110在线程构建的时候可以通过setPriority(int)方法来修改优先级默认优先级为5优先级高的线程分配时间片的数量要多于优先级低的线程。 设置线程优先级时针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级确保处理器不会被独占。在不同的JVM以及操作系统上线程规划会存在差异有些操作系统甚至会忽略对线程优先级的设定。
线程的调度 线程调度是指系统为线程分配CPU使用权的过程主要调度方式有两种协同式线程调度(Cooperative Threads-Scheduling)抢占式线程调度(Preemptive Threads-Scheduling)使用协同式线程调度的多线程系统 线程执行的时间由线程本身来控制 线程把自己的工作执行完之后 要主动通知系统切换到另外一个线程上。使用协同 式线程调度的最大好处是实现简单 由于线程要把自己的事情做完后才会通知系 统进行线程切换 所以没有线程同步的问题 但是坏处也很明显 如果一个线程 出了问题则程序就会一直阻塞。 使用抢占式线程调度的多线程系统 每个线程执行的时间以及是否切换都由 系统决定。在这种情况下 线程的执行时间不可控 所以不会有「一个线程导致 整个进程阻塞」的问题出现。
线程和协程 为什么Java线程调度是抢占式调度这需要我们了解Java中的线程实现模式。 我么已经知道线程其实是操作系统层面的实体Java中的线程怎么和操作系统层面对应起来呢任何语言实现线程主要有三种方式使用内核线程实现(1:1实现)使用用户线程实现(1:N实现)使用用户线程加轻量级进程混合实现(N:M实现)。
内核线程实现 使用内核线程实现的方式称为1:1实现。内核线程(Kernel-Level-Thread,KLT)就是直接有操作系统内核支持的线程这种线程由内核来完成线程切换内核通过操纵调度器(Scheduler)对线程进行调度并负责将线程的任务映射到各个处理器上。 由于内核线程的支持每个线程都称为一个独立的调度单元即使其中某一个在系统调用中被阻塞了也不会影响整个进程继续工作相关的调度工作也不需要额外考虑已经由操作系统处理了。 局限性首先由于是基于内核线程实现的所以各种线程操作如创建、析构及同步都需要进行系统调用。而系统调用的代价相对较高需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。其次每个语言层面的线程都需要有一个内核线程的支持因此要消耗一定的内核资源(如内核线程的栈空间)因此一个系统支持的线程数量是有限的。
用户线程实现 用户线程指的是完全建立在用户空间的线程库上系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成不需要内核的帮助。这种实现不需要切换到内核态因此操作很快而且低消耗也能够支持更大的线程数量部分高性能的数据库就是用这种方式实现的。 其劣势也在于没有系统内核的支持所有的线程操作都要求用户自己去完成。线程的创建、销毁、切换和调度都是用户需要考虑的问题。Java曾经是使用过用户线程最终又放弃了。但是近年来许多新的、以高并发为卖点的编程语言又普遍支持了用户线程譬如Golang。
混合实现 线程除了依赖内核线程和完全由用户线程自己实现之外还有一种将内核线程与用户线程一起使用的实现方式被称为NM实现。在这种混合实现下既存在用户线程也存在内核线程。 用户线程的创建、切换、析构等操作依然廉价并且可以支持大规模的用户线程并发。 在这种混合模式中用户线程与轻量级进程的数量比是不定的是NM的关系。
Java线程的实现 Java在JDK1.2以前是用永固线程实现的但从JDK1.3起Java虚拟机的线程模型普遍都被替换为基于操作系统原生线程模型来实现采用1:1的线程模型。 以HotSpot为例它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的所以HotSpot自己是不会去干涉线程调度的全权交给底下的操作系统去处理。 所以这就是我们说Java线程是抢占式的原因。
协程 1:1的内核线程模型是如今Java虚拟机实现的主流选择但是这种方式有天然的缺陷即切换、调度成本高系统能容纳的线程数量也很有限。现在在每个请求本身的执行时间变得很短、数量变得很多的前提下用户本身的业务线程切换的开销甚至可能会接近于计算本身的开销就会造成严重的浪费。 另外我们常见的Java Web服务器比如Tomcat的线程池容量通常在几十到两百之间当把数以百万计的请求往线程池里灌时系统即使处理得过来但其中的切换耗损也是相当可观的。 这样的话对Java语言来说用户线程的重新引入成为了上述问题一个非常可行的方案。其次Go语言等支持用户线程等新型语言给Java带来了巨大的压力也使得Java引入用户线程称为一个绕不开的话题。
协程简介 为什么用户线程又被称为协程呢我们知道内核线程的切换换开销是来自于 保护和恢复现场的成本 那如果改为采用用户线程 这部分开销就能够省略掉 吗 答案还是“不能”。 但是 一旦把保护、恢复现场及调度的工作从操作系 统交到程序员手上 则可以通过很多手段来缩减这些开销。 由于最初多数的用户线程是被设计成协同式调度Cooperati ve Scheduling 的所以它有了一个别名—— “协程”Corouti ne 完整地做调用栈的保护、 恢复工作所以今天也被称为“有栈协程”Stackfull Corouti ne。 协程的主要优势是轻量 无论是有栈协程还是无栈协程 都要比传统内核 线程要轻量得多。在 64 位 Linux 上 HotSpot 的线程栈容量默认是 1MB 此外内核数据结构Kernel Data Structures 还会额外消耗 16KB 内存。与之相对的 一个协程的栈通常在几百个字节到几KB 之间 所以 Java 虚拟机里线程池容量达到两百就已经不算小了 而很多支 持协程的应用中 同时并存的协程数量可数以十万计。 总的来说协程机制适用于被阻塞的且需要大量并发的场景网络 io 不适合大量计算的场景因为协程提供规模(更高的吞吐量)而不是速度(更低的 延迟)。
纤程-Java 中的协程 在 JVM 的实现上以 HotSpot 为例 协程的实现会有些额外的限制 Java 调用栈跟本地调用栈是做在一起的。 如果在协程中调用了本地方法 还能否正 常切换协程而不影响整个线程 另外如果协程中遇传统的线程同步措施会怎 样 譬如 Kotlin 提供的协程实现 一旦遭遇 synchronize 关键字 那挂起来的 仍将是整个线程。 OpenJDK 在 2018 年 创建了 Loom 项目这是 Java 的官方解决方案 并用了“纤程Fiber ”这个 名字。 Loom 项目背后的意图是重新提供对用户线程的支持 但这些新功能不是为 了取代当前基于操作系统的线程实现 而是会有两个并发编程模型在 Java 虚拟 机中并存 可以在程序中同时使用。 新模型有意地保持了与目前线程模型相似 的 API 设计 它们甚至可以拥有一个共同的基类 这样现有的代码就不需要为 了使用纤程而进行过多改动 甚至不需要知道背后采用了哪个并发编程模型。 根据 Loom 团队在 2018 年公布的他们对 Jett y 基于纤程改造后的测试结果 同样在 5000QPS 的压力下 以容量为 400 的线程池的传统模式和每个请求配以 一个纤程的新并发处理模式进行对比前者的请求响应延迟在 10000 至 20000 毫秒之间 而后者的延迟普遍在 200 毫秒以下。 目前 Java 中比较出名的协程Quasar[ˈkweɪzɑː®]Loom 项目的 Leader 就 是 Quasar 的作者Ron Pressler Quasar 的实现原理是字节码注入在字节码 层面对当前被调用函数中的所有局部变量进行保存和恢复。这种不依赖 Java 虚 拟机的现场保护虽然能够工作但影响性能。 2022 年 9 月 22 日JDK19非 LTS 版本 正式发布引入了协程并称为 轻量级虚拟线程。但是这个特性目前还是预览版 还不能引入生成环境。 因为环 境所限本课程不提供实际的范例只讲述基本用法和原理。
守护线程 Daemon守护 线程是一种支持型线程因为它主要被用作程序中后台调 度以及支持性工作。这意味着当一个 Java 虚拟机中不存在非 Daemon 线程的 时候 Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置 为 Daemon 线程。我们一般用不上比如垃圾回收线程就是Daemon 线程。 Daemon 线程被用作完成支持性工作 但是在 Java 虚拟机退出时 Daemon 线 程中的 finally 块并不一定会执行。在构建 Daemon 线程时 不能依靠 finally 块中 的内容来确保执行关闭或清理资源的逻辑。
线程间的通信和协调、 协作 很多的时候 孤零零的一个线程工作并没有什么太多用处 更多的时候 我 们是很多线程一起工作而且是这些线程间进行通信 或者配合着完成某项工作 这就离不开线程间的通信和协调、协作。
管道输入输出流 我们已经知道进程间有好几种通信机制其中包括了管道其实 Java 的 线程里也有类似的管道机制 用于线程之间的数据传输 而传输的媒介为内存。 设想这么一个应用场景通过 Java 应用生成文件 然后需要将文件上传到 云端比如
页面点击导出后 后台触发导出任务 然后将 mysql 中的数据根据导出 条件查询出来 生成 Excel 文件 然后将文件上传到 oss最后发步一个下载文 件的链接。和银行以及金融机构对接时从本地某个数据源查询数据后 上报 xml 格 式的数据给到指定的 ftp、或是 oss 的某个目录下也是类似的。 我们一般的做法是 先将文件写入到本地磁盘 然后从文件磁盘读出来上传 到云盘但是通过Java 中的管道输入输出流一步到位则可以避免写入磁盘这 一步。 Java 中的管道输入/输出流主要包括了如下 4 种具体实现PipedOutputStream 、PipedInputStream 、PipedReader 和 PipedWriter前两种面 向字节而后两种面向字符。
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;public class Piped {public static void main(String[] args) throws Exception {PipedWriter out new PipedWriter();PipedReader in new PipedReader();/* 将输出流和输入流进行连接否则在使用时会抛出IOException*/out.connect(in);Thread printThread new Thread(new Print(in), PrintThread);printThread.start();int receive 0;try {/*将键盘的输入用输出流接受在实际的业务中可以将文件流导给输出流*/while ((receive System.in.read()) ! -1){out.write(receive);}} finally {out.close();}}static class Print implements Runnable {private PipedReader in;public Print(PipedReader in) {this.in in;}Overridepublic void run() {int receive 0;try {/*输入流从输出流接收数据并在控制台显示*在实际的业务中可以将输入流直接通过网络通信写出 */while ((receive in.read()) ! -1){System.out.print((char) receive);}} catch (IOException ex) {}}}
}join方法 现在有 T1、T2、T3 三个线程 你怎样保证 T2 在 T1 执行完后执行 T3 在 T2 执行完后执行 答用 Thread#join 方法即可 在 T3 中调用 T2.join在 T2 中调用 T1.join。
join() 把指定的线程加入到当前线程 可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A 的 Join()方法 直到线程 A 执行完毕后 才会继续 执行线程 B 剩下的代码。
synchronized 内置锁 Java 支持多个线程同时访问一个对象或者对象的成员变量但是多个线程同 时访问同一个变量会导致不可预料的结果。关键字synchronized 可以修饰方法 或者以同步块的形式来进行使用 它主要确保多个线程在同一个时刻 只能有一 个线程处于方法或者同步块中 它保证了线程对变量访问的可见性和排他性 使 多个线程访问同一个变量的结果正确它又称为内置锁机制。
对象锁和类锁
对象锁是应用在对象上的锁类锁是应用在类上的锁如下便是类锁
private static synchronized void classLock() {...
}对象锁如下
private Object lock new Object();
private static objectLock() {synchronized(lock) {...}
}volatile最轻量的通信/同步机制 volatile 保证了不同线程对这个变量进行操作时的可见性即一个线程修改了某 个变量的值这新值对其他线程来说是立即可见的。 不加 volatile 时子线程无法感知主线程修改了 ready 的值从而不会退出循环 而加了 volatile 后子线程可以感知主线程修改了 ready 的值迅速退出循环。 但是 volatile 不能保证数据在多个线程下同时写时的线程安全volatile 最适用的 场景一个线程写多个线程读。
等待/通知机制 线程之间相互配合完成某项工作比如 一个线程修改了一个对象的值 而另一个线程感知到了变化然后进行相应的操作整个过程开始于一个线程 而最终执行又是另一个线程。前者是生产者 后者就是消费者 这种模式隔离了 “做什么”what和“怎么做”How简单的办法是让消费者线程不断地循环检查变量是否符合预期在 while 循环中设置不满足的条件 如果条件满足则退出 while循环从而完成消费者的工作。却存在如下问题
难以确保及时性。难以降低开销。如果降低睡眠的时间比如休眠1毫秒这样消费者能 更加迅速地发现条件 变化 但是却可能消耗更多的处理器资源 造成了无端的浪费。 等待/通知机制则可以很好的避免这种机制是指一个线程 A 调用了对象 O 的 wait()方法进入等待状态而另一个线程 B 调用了对象 O 的 notify()或者notifyAll()方法 线程 A 收到通知后从对象 O 的 wait()方法返回 进而执行后续操 作。上述两个线程通过对象 O 来完成交互 而对象上的 wait()和 notify/notifyAll() 的关系就如同开关信号一样用来完成等待方和通知方之间的交互工作。
notify() 通知一个在对象上等待的线程,使其从 wait 方法返回,而返回的前提是该线程 获取到了对象的锁 没有获得锁的线程重新进入 WAITING 状态。 notifyAll() 通知所有等待在该对象上的线程 wait() 调用该方法的线程进入WAITING 状态, 只有等待另外线程的通知或被中断才会返回. 需要注意,调用wait()方法后,会释放对象的锁 wait(long) 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有 通知就超时返回 wait(long,int) 对于超时时间更细粒度的控制,可以达到纳秒
等待和通知的标准范式 等待方遵循如下原则。
获取对象的锁。如果条件不满足 那么调用对象的 wait()方法 被通知后仍要检查条件。条件满足则执行对应的逻辑。
synchronized(对象) {while(条件不满足) {对象.wait();}对应的处理逻辑
}通知方遵循如下原则。 4. 获得对象的锁。 5. 改变条件。 6. 通知所有等待在对象上的线程。
synchronized(对象) {改变条件对象.notifyAll();
}在调用 wait()、notify()系列方法之前 线程必须要获得该对象的对象级 别锁 即只能在同步方法或同步块中调用 wait()方法、notify()系列方法进 入 wait()方法后 当前线程释放锁在从 wait()返回前 线程与其他线程竞争重新获得锁执行 notify()系列方法的线程退出调用了 notifyAll 的 synchronized 代码块的时候后他们就会去竞争。如果其中一个线程获得了该对象锁 它就会 继续往下执行 在它退出 synchronized 代码块释放锁后 其他的已经被唤醒的线程将会继续竞争获取该锁 一直进行下去 直到所有被唤醒的线程都执行完毕。
notify 和notifyAll 应该用谁
尽可能用 notifyall()谨慎使用 notify() 因为 notify()只会唤醒一个线程 我 们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程
方法和锁 调用 yield() 、sleep() 、wait() 、notify()等方法对锁有何影响 yield() 、sleep()被调用后都不会释放当前线程所持有的锁。 调用 wait()方法后会释放当前线程持有的锁而且当前被唤醒后 会重新 去竞争锁锁竞争到后才会执行 wait 方法后面的代码。 调用 notify()系列方法后 对锁无影响 线程只有在 syn 同步代码执行完后才 会自然而然的释放锁所以 notify()系列方法一般都是 syn 同步代码的最后一行。
wait 和notify
为什么 wait 和 notify 方法要在同步块中调用 主要是因为 Java API 强制要求这样做如果你不这么做你的代码会抛出IllegalMonitorStateException 异常。这个问题并不是说只在 Java 语言中会出现而是会在所有的多线程环境下出现。 为什么你应该在循环中检查等待条件? 处于等待状态的线程可能会收到错误警报和伪唤醒 如果不在循环中检查等待条件 程序就会在没有满足结束条件的情况下退出。因此 当一个等待线程醒来时不能认为它原来的等待状态仍然是有效的在 notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用 wait()方法效果更好的原因。
CompleteableFuture Java 的 1.5 版本引入了 Future可以把它简单的理解为运算结果的占位符 它提供了两个方法来获取运算结果。 get()调用该方法线程将会无限期等待运算结果。 get(longti meout, TimeUnit unit)调用该方法线程将仅在指定时间 timeout 内等待结果如果等待超时就会抛出 TimeoutExcepti on 异常。 Future 可以使用 Runnable 或 Callable 实例来完成提交的任务它存在如下几个问题
阻塞调用get()方法会一直阻塞直到等待直到计算完成它没有提供任何方法可以在完成时通知 同时也不具有附加回调函数的功能。链式调用和结果聚合处理 在很多时候我们想链接多个 Future 来完成耗时 较长的计算 此时需要合并结果并将结果发送到另一个任务中该接口很难完成 这种处理。异常处理 Future 没有提供任何异常处理的方式。 JDK1.8 才新加入的一个实现类 CompletableFuture 很好的解决了这些问题 CompletableFuture 实现了 Future Completi onStage两个接口。 实现了Future 接口意味着可以像以前一样通过阻塞或者轮询的方式获得结果。