12-JUC(上)
1. 引入#
1.1 进程和线程#
- 【进程】进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
- 【线程】通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
1.2 并发和并行#
1.3 线程状态#
public enum State {
NEW, // 新建
RUNNABLE, // 准备就绪
BLOCKED, // 阻塞
WAITING, // 不见不散
TIMED_WAITING, // 过时不候
TERMINATED; // 终结
}
wait 和 sleep 的功能都是当前线程暂停,有什么区别?
- 来自不同的类:wait ∈ Object,sleep ∈ Thread
- 关于锁的释放:wait 放开手去睡,放开手里的锁;sleep 握紧手去睡,醒了手里还有锁。相同点是均是在哪睡的在哪醒。
- 使用的范围不同:wait 必须在同步代码块中,sleep 可以在任何地方睡。
1.4 JUC 的三个包#
java.util.concurrent # 实用程序类通常在并发编程中很有用。
java.util.concurrent.atomic # 一个小型工具包,支持单个变量上的无锁线程安全编程。
java.util.concurrent.locks # 接口和类提供了一个框架,用于锁定和等待与内置同步和监视器不同的条件。
2. re: JDK 8#
2.1 Lambda#
/*
* 1. 函数式编程 f(x) = kx + 1;
* 2. 公式:拷贝小括号,写死右箭头,落地大括号
* 3. @FunctionalInterface 只有函数式接口才能用 Lambda 表达式
* 4. default/static 修饰的方法不碍事
*/
public class LambdaExpression {
public static void main(String[] args) {
/*
Foo foo = new Foo() {
@Override
public void sayHello() {
System.out.println("Hello! 1101");
}
};
foo.sayHello();
Foo foo = () -> {System.out.println("Hello! 1101");};
foo.sayHello();
*/
Foo foo = (int x, int y) -> {return x+y;};
System.out.println(foo.add(6, 7));
System.out.println(foo.mul(6, 7));
Foo.div(6, 7);
}
}
@FunctionalInterface
interface Foo {
// public void sayHello();
public int add(int x, int y);
// 可以定义多个
public default int mul(int x, int y) {
return x * y;
}
// 可以定义多个
public static int div(int x, int y) {
return x/y;
}
}
2.2 函数式接口#
public class MethodInterfaceTest {
public static void main(String[] args) {
// [消费] void accept(T t);
Consumer<String> consumer = (String t) -> {
System.out.println(t);
};
consumer.accept("LJQ");
// [供给] T get();
Supplier<String> supplier = () -> {
return "Keep Going!";
};
System.out.println(supplier.get());
// [函数] R apply(T t);
Function<Integer, Double> function = (Integer radius) -> {
return 2 * radius * 3.14;
};
System.out.println(function.apply(2));
// [断言] boolean test(T t);
Predicate<String> predicate = s -> {
return s.contains("a");
};
System.out.println(predicate.test("abc"));
}
}
2.3 Stream#
- 概念:流(Stream) 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
- 特点 // 集合讲的是数据,流讲的是计算!
- Stream 自己不会存储元素
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream。
- Stream 操作是延迟执行的,这意味着他们会等到需要结果的时候才执行。
- 阶段 // 源头 → 中间流水线 → 结果
- 起始操作:创建一个 Stream,即一个数据源(数组、集合)
- 中间操作:处理数据源数据
- 终止操作:执行中间操作链,产生结果
/*
请找出:
偶数ID
年龄大于24
用户名转为大写
用户名字母倒排序
只输出一个
用户名字
*/
public class StreamTest {
public static void main(String[] args) {
User u1 = new User(11, "aaa", 23);
User u2 = new User(12, "bbb", 24);
User u3 = new User(13, "ccc", 22);
User u4 = new User(14, "ddd", 28);
User u5 = new User(16, "eee", 26);
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
list.stream()
.filter(user -> { return user.getId()%2 == 0; })
.filter(user -> { return user.getAge() > 24; })
.map(user -> { return user.getUserName().toUpperCase(); })
.sorted((o1, o2) -> { return o2.compareTo(o1); })
.limit(1).forEach(System.out::println);
}
}
class User {
private Integer id;
private String userName;
private Integer age;
public User(Integer id, String userName, Integer age) {
this.id = id;
this.userName = userName;
this.age = age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
3. 卖票程序(Lock)#
题目:3 个售票员卖出 30 张票
解题:多线程编程模板 → [1] 在高内聚低耦合的前提下,线程-操作-资源类
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SaleTicketDemo {
// 主线程,一切程序的入口
public static void main(String[] args) {
// 先创建资源类
Ticket ticket = new Ticket();
// Thread(Runnable target, String name)
/*
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 40; i++) {
ticket.sale();
}
}
}, "A").start();
||
||
\/
@FunctionalInterface
public interface Runnable
*/
new Thread(() -> { for (int i = 0; i < 30; i++) ticket.sale(); }, "A").start();
new Thread(() -> { for (int i = 0; i < 30; i++) ticket.sale(); }, "B").start();
new Thread(() -> { for (int i = 0; i < 30; i++) ticket.sale(); }, "C").start();
}
}
/*
* 高内聚:想对资源进行什么操作,就必须在资源类里写操作的方法,而不能是外部提供。
* 低耦合:资源类对外暴露操作接口,资源调用者直接调用方法就行。
*/
class Ticket { // 资源类(空调) = [实例变量(温度值) + 实例方法(调高、调低)] -> 高内聚
private int number = 30; // (空调默认温度)
// List list = new ArrayList();
// 可重入锁
Lock lock = new ReentrantLock();
public void sale() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "\t卖出第:"
+ (number--) + "\t还剩下:" + number + "张");
}
} finally {
lock.unlock();
}
}
}
synchronized 和 Lock 区别:
4. 不安全的集合类#
4.1 示例代码#
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
/*
* 1. 故障现象
* java.util.ConcurrentModificationException
* 2. 导致原因
* 多线程争抢同一个资源,读写同时进行,且没有加锁
* 3. 解决方法
* × List<String> list = new Vector<>(); // 并发性 ←→ 数据一致性
* × List<String> list = Collections.synchronizedList(new ArrayList<>());
* √ List<String> list = new CopyOnWriteArrayList<>(); // 写时复制是“读写分离”思想的体现
* 4. 优化建议
* [JUC] CopyOnWriteArrayList
*/
public class NoSafeDemo {
@Test
public void mapNotSafe() {
// Map<String, String> map = new HashMap<>();
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
System.out.println(map);
}, "Thread" + i).start();
}
}
@Test
public void setNotSafe() {
// Set<String> set = new HashSet<>();
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(set);
}, "Thread" + i).start();
}
}
@Test
public void listNotSafe() {
// = new ArrayList<>();
// = new Vector<>();
// = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, "Thread" + i).start();
}
}
}
4.2 写时复制容器#
4.2.1 介绍#
CopyOnWriteArrayList 是 Java 并发包中提供的一个并发容器,它是个线程安全且读操作无锁的 ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为「写时复制器」,Java 并发包中类似的容器还有 CopyOnWriteSet。本文会对 CopyOnWriteArrayList 的实现原理及源码进行分析。
我们都知道,集合框架中的 ArrayList 是非线程安全的,Vector 虽是线程安全的,但由于简单粗暴的锁同步机制,性能较差。而 CopyOnWriteArrayList 则提供了另一种不同的并发处理策略(当然是针对特定的并发场景)。
很多时候,我们的系统应对的都是“读多写少”的并发场景。CopyOnWriteArrayList 容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。
4.2.2 源码分析#
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
// volatile 修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值
private transient volatile Object[] array;
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}
/**
* Creates an empty list.
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
public E get(int index) {
return get(getArray(), index);
}
/**
* CopyOnWriteArrayList 使用写时复制策略保证 list 的一致性,而 “获取–修改–写入”
* 三个步骤不是原子性,所以需要一个独占锁保证修改数据时只有一个线程能够进行,否则
* 多线程写的时候会 Copy 出 N 个副本出来。
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Appends the specified element to the end of this list.
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
}
CopyOnWriteArrayList 为什么并发安全且性能比 Vector 好?
Vector 是 CRUD 方法都加了 synchronized 以保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而 CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于 Vector,CopyOnWriteArrayList 支持“读多写少”的并发情况。
4.2.3 优缺点#
- 优点
- 读操作性能很高,因为无需任何同步措施,比较适用于“读多写少”的并发场景。Java 的 List 在遍历时,若中途有别的线程对 List 容器进行修改,则会抛出 ConcurrentModificationException 异常。
- 而 CopyOnWriteArrayList 由于其“读写分离”的思想,遍历和修改操作分别作用在不同的 List 容器,所以在使用迭代器进行遍历时候,也就不会抛出 ConcurrentModificationException 异常了。
- 缺点
- 内存占用问题,因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意!在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,有可能造成频繁的 Yong GC 和 Full GC。
- 无法保证实时性(数据一致性问题),Vector 对于读写操作均加锁同步,可以保证读和写的强一致性。而 CopyOnWriteArrayList 由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。即:CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据马上能读到,请不要使用 CopyOnWrite 容器。
- 注意点
- 减少扩容开销。根据实际需要,初始化容器的大小,避免写时容器扩容的开销。
- 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。
4.2.4 迭代器#
CopyOnWriteArrayList 提供了“弱一致性”的迭代器,从而保证在获取迭代器后,其他线程对 List 的修改是不可见的,迭代器遍历的数组是一个“快照”。
在调用 iterator() 后,会返回一个 COWIterator 对象,COWIterator 对象的 snapshot 变量保存了当前 List 的内容,cursor 是遍历 List 时数据的下标。
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/** 数组快照 */
private final Object[] snapshot;
/** 数组下标 */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
}
那么为什么说 snapshot 是 List 的快照呢,明明传的是引用。其实这就和 CopyOnWriteArrayList 本身有关了,如果在返回迭代器后没有对里面的数组 array 进行修改,则这两个变量指向的确实是同一个数组;但是若修改了,则根据前面所讲,它是会新建一个数组,然后将修改后的数组复制到新建的数组,而老的数组就会被“丢弃”,所以如果修改了数组,则此时 snapshot 指向的还是原来的数组,而 array 变量已经指向了新的修改后的数组了。这也就说明获取迭代器后,使用迭代器元素时其他线程对该 List 的增删改不可见,因为他们操作的是两个不同的数组,这就是「弱一致性」。
5. 八锁演示#
public class Lock8Demo {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
}, "A").start();
// Thread.sleep(100);
new Thread(() -> {
try {
phone.sendSMS();
// phone.sayHello();
// phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "B").start();
}
}
class Phone { // Phone.java -> Phone.class -> Class<Phone>
public void sayHello() {
System.out.println("Hello");
}
public synchronized void sendEmail() throws Exception {
// TimeUnit.SECONDS.sleep(3);
System.out.println("***** sendEmail *****");
}
public synchronized void sendSMS() throws Exception {
System.out.println("***** sendSMS *****");
}
}
1. 上述代码,请问先打印的是邮件还是短信?
不一定!这得看 OS 调度。
2. 暂停 3s 在邮件方法(放开 L14、L34 注释),请问先打印邮件还是短信?
正解:邮件。【原理】一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个 synchronized 方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些 synchronized 方法,锁的是当前对象 this!被锁定后,其它的线程都不能进入到当前对象的其它的 synchronized 方法。释放的是对象锁!
// 从 T3 开始,都是基于放开 L14、L34 注释进行测试的。
3. 线程 B 修改为调用普通成员方法 sayHello,请问先打印邮件还是 Hello?
正解:Hello。普通方法和同步锁无关
4. 两部手机,请问先打印邮件还是短信?
正解:不一定。换成两个对象后,就不是同一把锁了。
5. 两个静态同步方法,同一部手机,请问先打印邮件还是短信?
6. 两个静态同步方法,两部手机,请问先打印邮件还是短信?
正解:邮件。synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。具体表现为以下 3 种形式:
- 普通同步方法,锁是当前实例对象 this
- 静态同步方法,锁是当前类的 Class 对象
- 同步方法块,锁是 synchonized 括号里配置的对象
无论是几部手机,都是来自于同一个 Class<Phone>
对象。
7. 一个静态同步方法,一个普通同步方法,同一部手机,请问先打印邮件还是短信?
8. 一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信?
正解:短信。一个锁是 Class,一个锁是 this,不相干。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。也就是说,如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁 this,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是同一把锁 —— 类对象 Class 本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!
6. 生产者消费者#
多线程编程模板:
→ [1] 高内聚低耦合的前提下,线程操作资源类
→ [2] 判断 | 干活 | 通知
→ [3] 防止虚假唤醒
6.1 step1#
题目:现在有两个线程,可以操作初始值为 0 的一个变量,实现一个线程对该变量加 1,一个线程对该变量减一,交替 10 轮。
多线程编程模板 → [2] 判断(要不要干活) | 干活(生产/消费) | 通知(消费/生产)
public class ProviderAndConsumer {
public static void main(String[] args) throws Exception {
Aircondition ac = new Aircondition();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++ ) {
ac.increment();
}
} catch (Exception e) {
e.printStackTrace();
}
}, "Provider-1").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++ ) {
ac.decrement();
}
} catch (Exception e) {
e.printStackTrace();
}
}, "Consumer-1").start();
}
}
class Aircondition {
private int number = 0;
public synchronized void increment() throws Exception {
// 1. 判断(是否轮到我干活)
if (number != 0) {
this.wait();
}
// 2. 干活(生产)
number++;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3. 通知(消费)
this.notifyAll();
}
public synchronized void decrement() throws Exception {
// 1. 判断(是否轮到我干活)
if (number == 0) {
this.wait();
}
// 2. 干活(消费)
number--;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3. 通知(生产)
this.notifyAll();
}
}
打印结果:
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
6.2 step2#
修改题目:现新增两个线程,两个 Provider,两个 Consumer。
public class ProviderAndConsumer {
public static void main(String[] args) throws Exception {
Aircondition ac = new Aircondition();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++ ) {
ac.increment();
}
} catch (Exception e) {
e.printStackTrace();
}
}, "Provider-1").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++ ) {
ac.decrement();
}
} catch (Exception e) {
e.printStackTrace();
}
}, "Consumer-1").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++ ) {
ac.increment();
}
} catch (Exception e) {
e.printStackTrace();
}
}, "Provider-2").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++ ) {
ac.decrement();
}
} catch (Exception e) {
e.printStackTrace();
}
}, "Consumer-2").start();
}
}
打印结果:// 乱了,为啥 2 个 OK,4 个就不行了?
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-2 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-2 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-2 1
Consumer-2 0
Provider-2 1
Consumer-1 0
Provider-1 1
Provider-2 2
Consumer-2 1
Consumer-2 0
Provider-2 1
Provider-1 2
Provider-2 3
Consumer-2 2
Consumer-2 1
Consumer-2 0
Provider-2 1
Provider-1 2
Provider-2 3
Consumer-2 2
Consumer-2 1
Consumer-2 0
Provider-2 1
Consumer-2 0
多线程编程模板 → [3] 防止虚假唤醒:上面的代码问题出在 "判断" 上,注意,多线程的调度交互判断绝对不可以用 if,因为 if 只判断一次!像在一个参数版本中,中断和虚假唤醒是可能的,并且该方法应该始终在 循环(while) 中使用,while 是只要唤醒就要拉回来再判断一次。
synchronized (obj) {
// while:循环 + 判断
while (<condition does not hold>) obj.wait();
... // Perform action appropriate to condition
}
多线程的横向通信调用,判断用的是 while!修改代码:
class Aircondition {
private int number = 0;
public synchronized void increment() throws Exception {
// 1. 判断(是否轮到我干活)
while (number != 0) {
this.wait();
}
// 2. 干活(生产)
number++;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3. 通知(消费)
this.notifyAll();
}
public synchronized void decrement() throws Exception {
// 1. 判断(是否轮到我干活)
while (number == 0) {
this.wait();
}
// 2. 干活(消费)
number--;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3. 通知(生产)
this.notifyAll();
}
}
打印结果:
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-2 1
Consumer-1 0
Provider-1 1
Consumer-1 0
Provider-2 1
Consumer-2 0
Provider-1 1
Consumer-1 0
Provider-2 1
Consumer-2 0
Provider-1 1
Consumer-1 0
Provider-2 1
Consumer-2 0
Provider-1 1
Consumer-2 0
Provider-2 1
Consumer-2 0
Provider-2 1
Consumer-2 0
Provider-2 1
Consumer-2 0
Provider-2 1
Consumer-2 0
Provider-2 1
Consumer-2 0
Provider-2 1
Consumer-2 0
为什么会有 2?
6.3 step3#
多线程模板 + while + Lock(Condition)
Lock<I>
实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象 Condition 。
Lock 替换 synchronized 方法和语句的使用, Condition 取代了对象监视器方法(wait,notify 和 notifyAll)的使用。
一个 Condition 实例本质上绑定到一个锁。 要获得特定 Condition 实例,请使用其 newCondition()。
class Cake {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() throws Exception {
lock.lock();
try {
// 1. 判断(是否轮到我干活)
while (number != 0) {
condition.await();
}
// 2. 干活(生产)
number++;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3. 通知(消费)
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decrement() throws Exception {
lock.lock();
try {
// 1. 判断(是否轮到我干活)
while (number == 0) {
condition.await();
}
// 2. 干活(消费)
number--;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3. 通知(生产)
condition.signalAll();
} finally {
lock.unlock();
}
}
}
7. 线程间定制化调用通信#
7.1 Condition#
- Condition 中的 await() 相当于 Object 的 wait()
- Condition 中的 signal() 相当于 Object 的 notify()
- Condition 中的 signalAll() 相当于 Object 的 notifyAll()
不同的是,Object 中的这些方法是和同步锁捆绑使用的;而 Condition 是需要与互斥锁/共享锁捆绑使用的。
Condition 它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个 Condition,在不同的情况下使用不同的 Condition。例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,"读线程"需要等待。
如果采用 Object 类中的 wait()/notify()/notifyAll() 实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过 notify()/notifyAll() 明确的指定唤醒"读线程",而只能通过 notifyAll() 唤醒所有线程(但是 notifyAll() 无法区分唤醒的线程是读线程,还是写线程)。但通过 Condition,就能明确的指定唤醒读线程。
7.2 修改模板[2]#
多线程编程模板[2]:判断 → 干活 → 通知
- 有顺序的通知,需要有〈标志位〉
- 一个锁(Lock),配 N 把钥匙(Condition)
- 判断〈标志位〉
- 干活
- 修改〈标志位〉,通知下一个
7.3 示例代码#
(1)读写线程通知案例
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Buffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final int[] buffer;
private int count;
private int writeIndex;
private int readIndex;
public Buffer(int capacity) {
this.buffer = new int[capacity];
this.count = 0;
this.writeIndex = 0;
this.readIndex = 0;
}
public void write(int data) throws InterruptedException {
lock.lock();
try {
while (count == buffer.length) {
// Buffer is full, wait for notFull condition
notFull.await();
}
buffer[writeIndex] = data;
writeIndex = (writeIndex + 1) % buffer.length;
count++;
// Notify readers that buffer is not empty
notEmpty.signal();
} finally {
lock.unlock();
}
}
public int read() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
// Buffer is empty, wait for notEmpty condition
notEmpty.await();
}
int data = buffer[readIndex];
readIndex = (readIndex + 1) % buffer.length;
count--;
// Notify writers that buffer is not full
notFull.signal();
return data;
} finally {
lock.unlock();
}
}
}
(2)A 打印 5 次,B 打印 10 次, C 打印 15 次,按这个顺序来 10 轮(多线程之间按顺序调用:实现 A -> B -> C)。
/*
* 1. 有顺序的通知,需要有〈标志位〉
* 2. 一个锁(Lock),配 3 把钥匙(Condition)
* 3. 判断〈标志位〉
* 4. 干活:输出线程名 + 第 N 次 + 第几轮
* 5. 修改〈标志位〉,通知下一个
*/
public class ConditionDemo {
public static void main(String[] args) {
ShareData data = new ShareData();
new Thread(() -> {
for (int i = 0; i < 10; i++)
data.print5();
}, "1").start();
new Thread(() -> {
for (int i = 0; i < 10; i++)
data.print10();
}, "2").start();
new Thread(() -> {
for (int i = 0; i < 10; i++)
data.print15();
}, "3").start();
}
}
// 高内聚,低耦合
class ShareData {
// 标志位:A=1、B=2、C=3
private int number = 1; // 默认为 1,从 A 开始
private Lock lock = new ReentrantLock();
// 一把锁配多把钥匙
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5() {
lock.lock();
try {
// 1. 判断
while (number != 1) {
c1.await();
}
// 2. 干活
for (int i = 1; i <= 5; i++)
System.out.println("Thread-" + Thread.currentThread().getName()+ ": " + i);
// 3. 通知
// 3.1 一定要先改标志位!
number = 2;
// 3.2 如何精准通知到T2? → c2(一把锁配多把钥匙)
c2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
// 1. 判断
while (number != 2) {
c2.await();
}
// 2. 干活
for (int i = 1; i <= 10; i++)
System.out.println("Thread-" + Thread.currentThread().getName()+ ": " + i);
// 3. 通知
// 3.1 一定要先改标志位!
number = 3;
// 3.2 如何精准通知到T3? → c3(一把锁配多把钥匙)
c3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
// 1. 判断
while (number != 3) {
c3.await();
}
// 2. 干活
for (int i = 1; i <= 15; i++)
System.out.println("Thread-" + Thread.currentThread().getName()+ ": " + i);
// 3. 通知
// 3.1 一定要先改标志位!
number = 1;
// 3.2 如何精准通知到T1? → c1(一把锁配多把钥匙)
c1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
注意!必须是先修改!再通知!
通过合理使用 join(),也可以确保多个线程按特定顺序执行。具体来说,join() 方法使得一个线程在另一个线程完成之前保持等待状态。
public class ThreadJoinExample {
public static void main(String[] args) {
// 创建三个线程
Thread thread1 = new Thread(new Task("Task 1"));
Thread thread2 = new Thread(new Task("Task 2"));
Thread thread3 = new Thread(new Task("Task 3"));
try {
// 启动第一个线程
thread1.start();
// 等待第一个线程执行完毕
thread1.join();
// 启动第二个线程
thread2.start();
// 等待第二个线程执行完毕
thread2.join();
// 启动第三个线程
thread3.start();
// 等待第三个线程执行完毕
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 定义任务类
class Task implements Runnable {
private String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(name + " is running");
try {
// 模拟线程执行任务的时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " is completed");
}
}
8. CompletableFuture#
8.1 Future&Callable#
Future 接口定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。
package java.util.concurrent;
/**
* A {@code Future} represents the result of an asynchronous
* computation. Methods are provided to check if the computation is
* complete, to wait for its completion, and to retrieve the result of
* the computation. The result can only be retrieved using method
* {@code get} when the computation has completed, blocking if
* necessary until it is ready. Cancellation is performed by the
* {@code cancel} method. Additional methods are provided to
* determine if the task completed normally or was cancelled. Once a
* computation has completed, the computation cannot be cancelled.
* If you would like to use a {@code Future} for the sake
* of cancellability but not provide a usable result, you can
* declare types of the form {@code Future<?>} and
* return {@code null} as a result of the underlying task.
*
* <p>
* <b>Sample Usage</b> (Note that the following classes are all
* made-up.)
* <pre> {@code
* interface ArchiveSearcher { String search(String target); }
* class App {
* ExecutorService executor = ...
* ArchiveSearcher searcher = ...
* void showSearch(final String target)
* throws InterruptedException {
* Future<String> future
* = executor.submit(new Callable<String>() {
* public String call() {
* return searcher.search(target);
* }});
* displayOtherThings(); // do other things while searching
* try {
* displayText(future.get()); // use future
* } catch (ExecutionException ex) { cleanup(); return; }
* }
* }}</pre>
*
* The {@link FutureTask} class is an implementation of {@code Future} that
* implements {@code Runnable}, and so may be executed by an {@code Executor}.
* For example, the above construction with {@code submit} could be replaced by:
* <pre> {@code
* FutureTask<String> future =
* new FutureTask<String>(new Callable<String>() {
* public String call() {
* return searcher.search(target);
* }});
* executor.execute(future);}</pre>
*
* <p>Memory consistency effects: Actions taken by the asynchronous computation
* <a href="package-summary.html#MemoryVisibility"> <i>happen-before</i></a>
* actions following the corresponding {@code Future.get()} in another thread.
*
* @see FutureTask
* @see Executor
* @since 1.5
* @author Doug Lea
* @param <V> The result type returned by this Future's {@code get} method
*/
public interface Future<V> {
/**
* Attempts to cancel execution of this task. This attempt will
* fail if the task has already completed, has already been cancelled,
* or could not be cancelled for some other reason. If successful,
* and this task has not started when {@code cancel} is called,
* this task should never run. If the task has already started,
* then the {@code mayInterruptIfRunning} parameter determines
* whether the thread executing this task should be interrupted in
* an attempt to stop the task.
*
* <p>After this method returns, subsequent calls to {@link #isDone} will
* always return {@code true}. Subsequent calls to {@link #isCancelled}
* will always return {@code true} if this method returned {@code true}.
*
* @param mayInterruptIfRunning {@code true} if the thread executing this
* task should be interrupted; otherwise, in-progress tasks are allowed
* to complete
* @return {@code false} if the task could not be cancelled,
* typically because it has already completed normally;
* {@code true} otherwise
*/
boolean cancel(boolean mayInterruptIfRunning);
/**
* Returns {@code true} if this task was cancelled before it completed
* normally.
*
* @return {@code true} if this task was cancelled before it completed
*/
boolean isCancelled();
/**
* Returns {@code true} if this task completed.
*
* Completion may be due to normal termination, an exception, or
* cancellation -- in all of these cases, this method will return
* {@code true}.
*
* @return {@code true} if this task completed
*/
boolean isDone();
/**
* Waits if necessary for the computation to complete, and then
* retrieves its result.
*
* @return the computed result
* @throws CancellationException if the computation was cancelled
* @throws ExecutionException if the computation threw an
* exception
* @throws InterruptedException if the current thread was interrupted
* while waiting
*/
V get() throws InterruptedException, ExecutionException;
/**
* Waits if necessary for at most the given time for the computation
* to complete, and then retrieves its result, if available.
*
* @param timeout the maximum time to wait
* @param unit the time unit of the timeout argument
* @return the computed result
* @throws CancellationException if the computation was cancelled
* @throws ExecutionException if the computation threw an
* exception
* @throws InterruptedException if the current thread was interrupted
* while waiting
* @throws TimeoutException if the wait timed out
*/
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
【Quiz】获得多线程的方法几种?传统的是继承 Thread 类和实现 Runnable 接口,JDK5 以后又有实现 Callable 接口和 Java 的线程池获得。
创建新类 MyThread 实现 Callable 接口 // Callable 是一个函数式接口,因此也可以用作 Lambda 表达式或方法引用的赋值对象。
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1024;
}
}
Callable 接口与 Runnable 接口的区别?
- 是否有返回值
- 是否抛异常
- 落地方法不一样,一个是
run
,一个是call
如何使用 Callable 启动线程?先说明,Thread 类根本没有形参是 Callable 的构造器。
多态,一个类可以实现多个接口。FutureTask 类作为“中间人”,该类既实现了 Runnable 接口,又有形参为 Callable 的构造器。
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
public class FutureTask<V> implements RunnableFuture<V> {
public FutureTask(Callable<V> callable) {...}
public FutureTask(Runnable runnable, V result) {...}
}
使用 Callable 启动线程:
public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask, "A").start();
Integer result = futureTask.get();
System.out.println(result); // 1024
}
}
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 FutureTask 对象在后台完成,当主线程将来需要时,就可以通过 FutureTask 对象获得后台作业的计算结果或者执行状态。
一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一个 FutureTask 一旦计算完成,就不能再重新开始或取消计算(多次计算,多个 FutureTask)。get 方法结果只有在计算完成时能获取,否则会一直阻塞直到任务转入完成状态(所以要先用 isDone() 方法判断),然后会返回结果或者抛出异常。
举个生活中的例子:老师上着课,口渴了,去买水不合适,讲课线程继续,我可以单起个线程找班长帮忙买水,水买回来放桌上就行,老师需要的时候再去 get 水。
public class SimpleTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask(() -> {
System.out.println("Thread#[" + Thread.currentThread().getName() + "] is running ...");
TimeUnit.SECONDS.sleep(5);
return 1024;
});
new Thread(futureTask, "买水").start();
System.out.println("Thread#[讲课] is running ...");
// --- 阻塞
// System.out.println(futureTask.get());
// --- 非阻塞(异步) -> '轮询' 替代阻塞
while (true) {
if (futureTask.isDone()) {
System.out.println(futureTask.get());
break;
} else {
System.out.println("task is not completed.");
}
}
}
}
轮询的方式会耗费无谓的 CPU 资源,而且也不见得能及时地得到计算结果。但如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞。
如果此时想完成一些复杂的任务(如下),FutureTask 是做不了的,由此引入 CompletableFuture。
- 应对 Future 的完成时间,完成了可以告诉我,也就是我们的回调通知;
- 将两个异步计算合成一个异步计算,这两个异步计算互相独立,同时第二个又依赖第一个的结果;
- 当 Future 集合中某个任务最快结束时,返回结果;
- 等待Future 集合中的所有任务都完成;
- ...
8.2 CompletableFuture#
在 Java8 中,CompletableFuture 提供了非常强大的 Future 的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合 CompletableFuture 的方法。
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> { ... }
它可能代表一个明确完成的 Future,也有可能代表一个完成阶段(CompletionStage),它支持在计算完成以后触发一些函数或执行某些动作。
CompletionStage 表示异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似 Linux 系统的管道分隔符传参数。
a. 异步 API#
CompletableFuture 提供了 4 个静态方法来创建一个异步操作。
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
API 调用示例(如果不手动提供,CompletableFuture 执行的时候默认使用的是守护线程池):
public void api() throws ExecutionException, InterruptedException {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {
// ForkJoinPool.commonPool-worker-1 come in!
System.out.println(Thread.currentThread().getName() + " come in!");
});
System.out.println(completableFuture1.get());
CompletableFuture<Void> completableFuture2 = CompletableFuture.runAsync(() -> {
// pool-1-thread-1 come in!
System.out.println(Thread.currentThread().getName() + " come in!");
}, threadPool);
System.out.println(completableFuture2.get());
CompletableFuture<Integer> completableFuture3 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + " come in!");
return 1101;
});
System.out.println(completableFuture3.get());
CompletableFuture<Integer> completableFuture4 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + " come in!");
return 1024;
}, threadPool);
System.out.println(completableFuture4.get());
threadPool.shutdown();
}
从 Java8 开始引入的 CompletableFuture 是 Future 的功能增强版,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法,减少阻塞和轮询。
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1101;
})
.thenApply(fn -> fn + 1024)
.whenComplete((var, e) -> {
if (e == null) {
System.out.println("=> result: " + var);
}
})
.exceptionally(throwable -> {
throwable.printStackTrace();
return null;
})
.join();
System.out.println("--------- main");
// 主线程不要立刻结束,否则 CompletableFuture 默认使用的线程池会立刻关闭: 暂停 3s 线程
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
b. 比价案例#
经常出现在等待某条 SQL 执行完成后,再继续执行下一条 SQL ,而这两条 SQL 本身是并无关系的,可以同时进行执行的。我们希望能够两条 SQL 同时进行处理,而不是等待其中的某一条 SQL 完成后,再继续下一条。同理,对于分布式微服务的调用,按照实际业务,如果是无关联的 step by step 业务,可以尝试是否可以多箭齐发,同时调用。
【场景】我们去比同一个商品在各个平台上的价格,要求获得一个清单列表,思路如何?
- 查完京东查淘宝,查完淘宝查天猫 ...
- 一口气同时查询 ...
public class ComparePriceTest {
static List<NetMall> mallList = Arrays.asList(
new NetMall("jd"), new NetMall("pdd"), new NetMall("tmall"),
new NetMall("du"), new NetMall("suning"), new NetMall("shopee"));
/**
* step by step
*
* @param productName
* @return
*/
public static List<String> getPriceByStep(String productName) {
return mallList.stream()
.map(mall -> String.format("[%s] %s's price is %.2f", mall, productName, mall.calcPrice(productName)))
.collect(Collectors.toList());
}
/**
* async
*
* @param productName
* @return
*/
public static List<String> getPriceByAsync(String productName) {
return mallList.stream()
.map(mall -> CompletableFuture.supplyAsync(
() -> String.format("[%s] %s's price is %.2f", mall, productName, mall.calcPrice(productName)))
).collect(Collectors.toList())
.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
public static void main(String[] args) {
long startTime1 = System.currentTimeMillis();
getPriceByStep("Java Concurrency in Practice").forEach(item -> System.out.println(item));
long endTime1 = System.currentTimeMillis();
System.out.println("[sync] " + (endTime1 - startTime1)); // 6097
long startTime2 = System.currentTimeMillis();
getPriceByAsync("Java Concurrency in Practice").forEach(item -> System.out.println(item));
long endTime2 = System.currentTimeMillis();
System.out.println("[async] " + (endTime2 - startTime2)); // 1008
}
static class NetMall {
private String mallName;
public String getMallName() {
return this.mallName;
}
public NetMall(String mallName) {
this.mallName = mallName;
}
public double calcPrice(String productName) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
}
}
}
控制台打印(功能 → 性能):
[io.tree6x7.future.ComparePriceTest$NetMall@3feba861] Java Concurrency in Practice's price is 75.06
[io.tree6x7.future.ComparePriceTest$NetMall@5b480cf9] Java Concurrency in Practice's price is 74.94
[io.tree6x7.future.ComparePriceTest$NetMall@6f496d9f] Java Concurrency in Practice's price is 74.60
[io.tree6x7.future.ComparePriceTest$NetMall@723279cf] Java Concurrency in Practice's price is 74.93
[io.tree6x7.future.ComparePriceTest$NetMall@10f87f48] Java Concurrency in Practice's price is 74.70
[io.tree6x7.future.ComparePriceTest$NetMall@b4c966a] Java Concurrency in Practice's price is 74.34
[sync] 6086
[io.tree6x7.future.ComparePriceTest$NetMall@3feba861] Java Concurrency in Practice's price is 75.07
[io.tree6x7.future.ComparePriceTest$NetMall@5b480cf9] Java Concurrency in Practice's price is 75.34
[io.tree6x7.future.ComparePriceTest$NetMall@6f496d9f] Java Concurrency in Practice's price is 74.11
[io.tree6x7.future.ComparePriceTest$NetMall@723279cf] Java Concurrency in Practice's price is 75.30
[io.tree6x7.future.ComparePriceTest$NetMall@10f87f48] Java Concurrency in Practice's price is 74.22
[io.tree6x7.future.ComparePriceTest$NetMall@b4c966a] Java Concurrency in Practice's price is 75.65
[async] 1007
8.3 常用方法#
a. 获得结果和触发计算#
/**
* 获得结果和触发计算
*
* @throws InterruptedException
* @throws ExecutionException
*/
private static void m1() throws InterruptedException, ExecutionException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
});
// Waits if necessary for this future to complete, and then returns its result.
// System.out.println(future.get());
// Waits if necessary for at most the given time for this future to complete, and then returns its result, if available.
// throws TimeoutException if the wait timed out
// System.out.println(future.get(2, TimeUnit.SECONDS));
// Returns the result value (or throws any encountered exception) if completed, else returns the given valueIfAbsent.
// System.out.println(future.getNow(1101));
// If not already completed, sets the value returned by get() and related methods to the given value.
System.out.println(future.complete(2) + "--->" + future.get());
}
b. 对计算结果进行处理#
/**
* 对计算结果进行处理
*
* @throws InterruptedException
*/
private static void m2() throws InterruptedException {
System.out.println(CompletableFuture.supplyAsync(() -> {
System.out.println("------ 1");
return 1;
}).thenApply(previousRet -> {
System.out.println("------ 2");
int i = 10 / 0; // => 1,2,4,5
return previousRet += 2;
// 将下一步的 thenApply 换成 handle => 1,2,3,4,5
}).handle((previousRet, ex) -> {
System.out.println("------ 3");
return previousRet += 3;
}).whenComplete((ret, ex) -> {
System.out.println("------ 4");
if (ex == null) {
System.out.println("=> final result: " + ret);
}
}).exceptionally(ex -> {
System.out.println("------ 5");
ex.printStackTrace();
return null;
}).join());
}
c. 对计算结果进行消费#
/**
* 对计算结果进行消费
*/
private static void m3() {
System.out.println(CompletableFuture.supplyAsync(() -> "ret-A").thenRun(() -> {
System.out.println("thenRun");
}).join());
System.out.println(CompletableFuture.supplyAsync(() -> "ret-A").thenAccept(retA -> {
System.out.println("thenAccept");
}).join());
System.out.println(CompletableFuture.supplyAsync(() -> "ret-A").thenApply(retA -> {
return retA + "~ret-B";
}).join());
}
d. 对计算速度进行选用#
/**
* 对计算速度进行选用
*/
private static void m4() {
Integer winner = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1101;
}).applyToEither(CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1024;
}), fn -> {
System.out.print("Who is mother fucking best?");
return fn;
}).join();
System.out.println(winner);
}
e. 对计算结果进行合并#
/**
* 对计算结果进行合并
*/
private static void m5() {
Integer ret = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + ": 查询基本数据");
return 1;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + ": 查询抓拍记录");
return 2;
}), (ret1, ret2) -> {
System.out.println(Thread.currentThread().getName() + ": step1, step2 结果合并");
return ret1 + ret2;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + ": 查询聚合数据");
return 3;
}), (ret12, ret3) -> {
System.out.println(Thread.currentThread().getName() + ": step1-2, step3 结果合并");
return ret12 + ret3;
}).join();
// 6
System.out.println(ret);
}
控制台打印:
ForkJoinPool.commonPool-worker-1: 查询基本数据
ForkJoinPool.commonPool-worker-2: 查询抓拍记录
main: step1, step2 结果合并
ForkJoinPool.commonPool-worker-2: 查询聚合数据
main: step1-2, step3 结果合并
6
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?