从0开始深入理解并发、线程与等待通知机制(下)

一、synchronized 内置锁

Java 支持多个线程同时访问一个对象或者对象的成员变量,但是多个线程同时访问同一个变量,会导致不可预料的结果。关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,使多个线程访问同一个变量的结果正确,它又称为内置锁机制

对象锁和类锁

对象锁用于对象实例方法,或者一个对象实例上的类锁是用于类的静态方法或者一个类的 class 对象上的
在这里插入图片描述
比如上面的 synClass 方法就使用了类锁
我们知道,类的对象实例可以有很多个,所以当对同一个变量操作时,用来做锁的对象必须是同一个,否则加锁毫无作用。比如下面的示例代码:
在这里插入图片描述

但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的 class 对象,但是每个类只有一个 class 对象,所以每个类只有一个类锁

同样的,当对同一个变量操作时,类锁和对象(非 class 对象)锁混用也同样毫无用处

错误的加锁和原因分析

在这里插入图片描述
执行结果
在这里插入图片描述

可以看到 i 的取值会出现乱序或者重复取值的现象
原因:虽然我们对 i 进行了加锁,但是
在这里插入图片描述

当我们反编译这个类的 class 文件后,可以看到 i++实际是
在这里插入图片描述
本质上是返回了一个新的 Integer 对象。也就是每个线程实际加锁的是不同的 Integer 对象,所以说到底,还是当对同一个变量操作时,用来做锁的对象必须是同一个,否则加锁毫无作用

二、volatile,最轻量的通信/同步机制

volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
在这里插入图片描述
在这里插入图片描述

不加 volatile 时,子线程无法感知主线程修改了 ready 的值,从而不会退出循环,而加了 volatile 后,子线程可以感知主线程修改了 ready 的值,迅速退出循环
但是 volatile 不能保证数据在多个线程下同时写时的线程安全,volatile 最适用的
场景:一个线程写,多个线程读

三、等待/通知机制

线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了“做什么”(what)和“怎么做”(How),简单的办法是让消费者线程不断地循环检查变量是否符合预期在 while 循环中设置不满足的条件,如果条件满足则退出 while 循环,从而完成消费者的工作却存在如下问题
1)难以确保及时性
2)难以降低开销。如果降低睡眠的时间,比如休眠 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):对于超时时间更细粒度的控制,可以达到纳秒

等待和通知的标准范式

等待方遵循如下原则:
1)获取对象的锁。
2)如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。

在这里插入图片描述

通知方遵循如下原则:
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。

在这里插入图片描述
在调用 wait ()、notify() 系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait ()方法、notify() 系列方法进入 wait()方法后,当前线程释放锁,在从 wait()返回前,线程与其他线程竞争重新获得锁,执行 notify()系列方法的线程退出调用了 notifyAll 的 synchronized代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕

notify 和 notifyAll 应该用谁

尽可能用 notifyall(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程

等待超时模式实现一个连接池

调用场景:调用一个方法时等待一段时间(一般来说是给定一个时间段),如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超时返回默认结果。
假设等待时间段是 T,那么可以推断出在当前时间 now+T 之后就会超时
等待持续时间:REMAINING=T。
•超时时间:FUTURE=now+T。
在这里插入图片描述
在这里插入图片描述
客户端获取连接的过程被设定为等待超时的模式,也就是在 1000 毫秒内如果无法获取到可用连接,将会返回给客户端一个 null。设定连接池的大小为 10个,然后通过调节客户端的线程数来模拟无法获取连接的场景。

它通过构造函数初始化连接的最大上限,通过一个双向队列来维护连接,调用方需要先调用 fetchConnection(long)方法来指定在多少毫秒内超时获取连接,当连接使用完成后,需要调用 releaseConnection(Connection)方法将连接放回线程池

四、面试题

方法和锁

调用 yield() 、sleep()、wait()、notify()等方法对锁有何影响?
yield() 、sleep()被调用后,都不会释放当前线程所持有的锁
调用 wait()方法后,会释放当前线程持有的锁,而且当前线程被唤醒后,会重新去竞争锁,锁竞争到后才会执行 wait 方法后面的代码。调用 notify()系列方法后,对锁无影响,线程只有在 syn 同步代码执行完后才会自然而然的释放锁,所以 notify()系列方法一般都是 syn 同步代码的最后一行

sync用静态对象当锁和放在静态方法上一样吗?
在这里插入图片描述
并不是,因为锁在静态方法 锁的是类的class对象,显然锁的对象不一致

wait 和 notify

为什么 wait 和 notify 方法要在同步块中调用?
主要是因为 Java API 强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException 异常。其实真实原因是:
这个问题并不是说只在 Java 语言中会出现,而是会在所有的多线程环境下出现。
假如我们有两个线程,一个消费者线程,一个生产者线程。生产者线程的任务可以简化成将 count 加一,而后唤醒消费者;消费者则是将 count 减一,而后在减到 0 的时候陷入睡眠
生产者伪代码:
count+1;
notify();
消费者伪代码:
while(count<=0)
wait()
count–

这里面有问题。什么问题呢?

生产者是两个步骤
1.count+1;
2. notify();
消费者也是两个步骤
1.检查 count 值;
2. 睡眠或者减一;

万一这些步骤混杂在一起呢?比如说,初始的时候 count 等于 0,这个时候消费者检查 count 的值,发现 count 小于等于 0 的条件成立;就在这个时候,发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了,也就是发出了通知,准备唤醒一个线程。这个时候消费者刚决定睡觉,还没睡呢,所以这个通知就会被丢掉。紧接着,消费者就睡过去了……
在这里插入图片描述
这就是所谓的 lost wake up 问题。
那么怎么解决这个问题呢?

现在我们应该就能够看到,问题的根源在于,消费者在检查 count 到调用wait()之间,count 就可能被改掉了。这就是一种很常见的竞态条件。很自然的想法是,让消费者和生产者竞争一把锁,竞争到了的,才能够修改count 的值。

为什么你应该在循环中检查等待条件?
处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在 notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用 wait()方法效果更好的原因。

五、CompleteableFuture

Java 的 1.5 版本引入了 Future,可以把它简单的理解为运算结果的占位符,它提供了两个方法来获取运算结果。
get()调用该方法线程将会无限期等待运算结果
get(long timeout, TimeUnit unit)调用该方法线程将仅在指定时间 timeout内等待结果,如果等待超时就会抛出 TimeoutException 异常
Future 可以使用 Runnable 或 Callable 实例来完成提交的任务,它存在如下几个问题:
1.阻塞 调用 get() 方法会一直阻塞,直到等待直到计算完成,它没有提供任何方法可以在完成时通知,同时也不具有附加回调函数的功能
2.链式调用和结果聚合处理 在很多时候我们想链接多个 Future 来完成耗时较长的计算,此时需要合并结果并将结果发送到另一个任务中,该接口很难完成这种处理
3.异常处理 Future 没有提供任何异常处理的方式
JDK1.8 才新加入的一个实现类 CompletableFuture,很好的解决了这些问题,CompletableFuture 实现了 Future, CompletionStage两个接口。实现了Future 接口,意味着可以像以前一样通过阻塞或者轮询的方式获得结果。
在这里插入图片描述

创建

除了直接 new 出一个 CompletableFuture 的实例,还可以通过工厂方法创建CompletableFuture 的实例

在这里插入图片描述

在这里插入图片描述

Asynsc 表示异步,而 supplyAsyncrunAsync 不同在于,supplyAsync 异步返回一个结果,runAsync 是 void第二个函数第二个参数表示是用我们自己创建的线程池,否则采用默认的 ForkJoinPool.commonPool()作为它的线程池

获得结果的方法
public T get()
public T get(long timeout, TimeUnit unit)
public T getNow(T valueIfAbsent)
public T join()
在这里插入图片描述
在这里插入图片描述

getNow 有点特殊,如果结果已经计算完则返回结果或者抛出异常,否则返回给定的 valueIfAbsent 值
join 返回计算的结果或者抛出一个 unchecked 异常(CompletionException),它和 get 对抛出的异常的处理有些细微的区别。

辅助方法
public static CompletableFuture allOf(CompletableFuture<?>... cfs) public static CompletableFuture **anyOf**(CompletableFuture<?>… cfs)
在这里插入图片描述
在这里插入图片描述

allOf 方法是当所有的 CompletableFuture 都执行完后执行计算。
anyOf 方法是当任意一个 CompletableFuture 执行完后就会执行计算,计算的结果相同

在这里插入图片描述

CompletionStage 是一个接口,从命名上看得知是一个完成的阶段,它代表了一个特定的计算的阶段,可以同步或者异步的被完成。你可以把它看成一个计算流水线上的一个单元,并最终会产生一个最终结果,这意味着几个CompletionStage 可以串联起来,一个完成的阶段可以触发下一阶段的执行,接着触发下一次,再接着触发下一次,……….。

总结 CompletableFuture 几个关键点:
1、计算可以由 Future ,Consumer 或者 Runnable 接口中的 apply,accept或者 run 等方法表示
2、计算的执行主要有以下
a. 默认执行
b. 使用默认的 CompletionStage 的异步执行提供者异步执行。这些方法名使用 someActionAsync 这种格式表示
c. 使用 Executor 提供者异步执行。这些方法同样也是 someActionAsync 这种格式,但是会增加一个 Executor 参数
CompletableFuture 里大约有五十种方法,但是可以进行归类,

变换类 thenApply
在这里插入图片描述

关键入参是函数式接口 Function。它的入参是上一个阶段计算后的结果,返回值是经过转化后结果

消费类 thenAccept
在这里插入图片描述

关键入参是函数式接口 Consumer。它的入参是上一个阶段计算后的结果,没有返回值

执行操作类 thenRun
在这里插入图片描述

对上一步的计算结果不关心,执行下一个操作,入参是一个 Runnable 的实例,表示上一步完成后执行的操作

结合转化类: :
在这里插入图片描述

需要上一步的处理返回值,并且 other 代表的 CompletionStage 有返回值之后,利用这两个返回值,进行转换后返回指定类型的值。
两个 CompletionStage 是并行执行的,它们之间并没有先后依赖顺序,other并不会等待先前的 CompletableFuture 执行完毕后再执行。

结合 转化 类
在这里插入图片描述

对于 Compose 可以连接两个 CompletableFuture,其内部处理逻辑是当第一个 CompletableFuture 处理没有完成时会合并成一个CompletableFuture,如果处理完成,第二个 future 会紧接上一个 CompletableFuture 进行处理。
第一个 CompletableFuture 的处理结果是第二个 future 需要的输入参数

结合 消费类: :
在这里插入图片描述

需要上一步的处理返回值,并且 other 代表的 CompletionStage 有返回值之后,利用这两个返回值,进行消费

运行后执行类
在这里插入图片描述

不关心这两个 CompletionStage 的结果,只关心这两个 CompletionStage 都执行完毕,之后再进行操作(Runnable)。

取最快转换类
在这里插入图片描述

两个 CompletionStage,谁计算的快,我就用那个 CompletionStage 的结果进行下一步的转化操作。现实开发场景中,总会碰到有两种渠道完成同一个事情,所以就可以调用这个方法,找一个最快的结果进行处理。

取最快消费类
在这里插入图片描述

两个 CompletionStage,谁计算的快,我就用那个 CompletionStage 的结果进行下一步的消费操作

取最快运行后执行类
在这里插入图片描述

两个 CompletionStage,任何一个完成了都会执行下一步的操作(Runnable)。

异常补偿类
在这里插入图片描述

当运行时出现了异常,可以通过 exceptionally 进行补偿。

运行 后 记录结果 类
在这里插入图片描述

action 执行完毕后它的结果返回原始的 CompletableFuture 的计算结果或者返回异常。所以不会对结果产生任何的作用

运行后处理结果类
在这里插入图片描述

运行完成时,对结果的处理。这里的完成时有两种情况,一种是正常执行,返回值。另外一种是遇到异常抛出造成程序的中断。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值