文章目录
- 1 什么是 Executor 和 ExecutorService ?这两个接口有什么区别?
- 2 java.util.concurrent 标准库中 ExecutorService 的可用实现是什么 ?
- 3 什么是 Java 内存模型( JMM )?描述下其目的和基本思想
- 4 JMM 对添加了 final 修饰符的类的字段有什么特殊保证 ?
1 什么是 Executor 和 ExecutorService ?这两个接口有什么区别?
Executor
和 ExecutorService
是用于处理并发任务的两个接口。
Executor
主要用于基本的任务提交,适用于简单的线程管理需求,而ExecutorService
提供了更多的功能,适用于需要更复杂的任务控制和线程管理的场景。
Executor
是一个简单的接口,它只定义了一个方法 execute(Runnable command)
,用于执行 Runnable
对象,这个方法将 Runnable
对象提交给执行器,然后执行器在适当的线程上运行这个任务。
// 使用一个固定大小的线程池来执行任务
Executor executor = Executors.newFixedThreadPool(10);
executor.execute(() -> System.out.println("任务运行ing"));
ExecutorService
继承自 Executor
,提供了更多的功能,它允许在执行任务时进行更多的控制和管理,比如提交 Callable
任务、取消任务和关闭执行器等。ExecutorService
提供的方法包括 submit()
用于提交任务并获得结果,shutdown()
用于平稳关闭执行器等。
ExecutorService executorService = Executors.newFixedThreadPool(10);
// submit() 方法提交了一个 Callable 任务,并返回一个 Future 对象,用于获取任务的结果
Future<Integer> future = executorService.submit(() -> {
// 任务逻辑
return 123;
});
// 用于关闭执行器,停止接受新任务并在所有已提交任务完成后关闭线程池
executorService.shutdown();
2 java.util.concurrent 标准库中 ExecutorService 的可用实现是什么 ?
ExecutorService
提供了多种实现,每种实现都有其特定的用途和特点,常见的实现包括ThreadPoolExecutor
、ScheduledThreadPoolExecutor
和ForkJoinPool
。
ThreadPoolExecutor
是最基础的实现,它通过维护一个线程池来处理任务。可以创建固定大小的线程池,也可以设置线程池的最大和最小线程数。
使用 Executors.newFixedThreadPool(int nThreads)
方法创建一个固定线程数的线程池:
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> System.out.println("任务执行ed"));
ScheduledThreadPoolExecutor
扩展了 ThreadPoolExecutor
,支持任务的定时和周期性执行。
使用 Executors.newScheduledThreadPool(int corePoolSize)
可以创建一个支持定时任务的线程池:
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
scheduler.scheduleAtFixedRate(() -> System.out.println("定时任务"), 0, 1, TimeUnit.SECONDS);
ForkJoinPool
专注于大规模并行计算,适用于分而治之的任务模型,它特别适合处理递归任务,可以高效地将任务分解成多个子任务,然后将结果合并。
使用 ForkJoinPool
可以执行分解任务的操作:
ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.submit(() -> {
// 递归任务逻辑
}).join();
3 什么是 Java 内存模型( JMM )?描述下其目的和基本思想
Java 内存模型(JMM)可以比喻成一个图书馆的借书系统。这个系统的目的是确保所有的借书和还书记录在所有的图书馆分馆之间都是一致的。无论你在哪个分馆借书或还书,系统都会确保你得到的是最新的、准确的书籍记录。
Java 内存模型(JMM)定义了 Java 程序中线程如何与内存进行交互的规则,它的主要目的是确保多线程环境下的程序行为是可预测的,避免由于线程间的并发访问导致的不一致性和不可预期的错误。
JMM 主要关注两个方面:内存可见性和指令重排序。
内存可见性就像图书馆的借书记录需要在所有分馆同步一样,JMM 确保当一个线程修改了共享数据,其他线程能及时看到这个修改。
内存可见性指的是当一个线程修改了共享变量的值,其他线程能够及时看到这个变化。为了保证这一点,JMM 规定了在不同线程之间的数据同步必须通过特定的操作,比如使用 synchronized
关键字或 volatile
关键字。
使用 synchronized
能够确保在进入一个同步块时,所有之前对共享变量的修改对当前线程是可见的;同样,当线程退出同步块时,对共享变量的修改也会被及时刷新到主内存中。
private int counter = 0;
public synchronized void increment() {
counter++;
}
public synchronized int getCounter() {
return counter;
}
指令重排序就像图书馆系统需要确保借书和还书的记录按正确的顺序处理。
指令重排序是指编译器或处理器可能会对程序中的指令进行重新排列,以提高性能。虽然这种优化对于单线程程序通常是透明的,但在多线程环境中可能导致意外的行为。
JMM 通过引入内存屏障和 volatile
关键字来规范指令执行的顺序,从而避免指令重排序带来的问题。
声明一个变量为 volatile
可以确保这个变量的读写操作不会被重排序:
private volatile boolean flag = false;
public void setFlag() {
flag = true;
}
public boolean checkFlag() {
return flag;
}
JMM 通过这些规则和机制来实现多线程程序的正确性和一致性,确保程序在并发执行时能够以预期的方式运行,而不受硬件和编译器优化的影响,使得 Java 的多线程编程更具可预测性和可靠性。
4 JMM 对添加了 final 修饰符的类的字段有什么特殊保证 ?
把
final
字段比作一个已经封好的信封。假设你把一封信(即final
字段)写好并封好(初始化完成),然后把它交给别人(其他线程)。一旦信封封好并且交给了别人,信封中的内容就不会再改变。其他人只要拿到这个信封,就可以确信里面的内容是完整和准确的,没有被修改过的可能性。
在 Java 内存模型中,给字段添加 final
修饰符会带来特别的保证,final
修饰的字段在对象构造完成后,会有一项确保该字段不会被改变的约束。
这样做的好处是,它保证了在构造器中对该字段的所有写入操作在其他线程看到对象时都是可见的,且这些写入操作不会被重排序,确保了对象的正确初始化。
有一个类 Person
,它有一个 final
字段 name
:
public class Person {
// Person 对象创建完成,name 的值就不能再改变
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Java 的内存模型保证了在对象的构造完成后,所有线程都能看到 name
字段的正确值。这是因为构造完成后,final
字段的值写入将会被正确地发布到其他线程中,避免了由于内存重排序或缓存导致的读取不一致。
特别地,当一个对象被完全构造好并且对外可见时,所有 final
字段的值都已被正确地初始化。
这种保证也意味着,如果一个线程看到一个对象的 final
字段,那么它也能看到该对象的其他 final
字段及构造过程中的所有必要的初始化步骤。这就是 final
关键字在多线程环境中提供的内存可见性保证,使得 final
字段成为线程安全的。
在人生的旅途中,一定要学会自己拯救自己,这样才能在逆境中奋勇前行