文章目录
一、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 表示异步,而 supplyAsync 与 runAsync 不同在于,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 的计算结果或者返回异常。所以不会对结果产生任何的作用。
运行后处理结果类:
运行完成时,对结果的处理。这里的完成时有两种情况,一种是正常执行,返回值。另外一种是遇到异常抛出造成程序的中断。