第一节 线程池的复习
- 为什么线程池用阻塞队列不用非阻塞队列?(用BlockingQueue)
因为当我们把超过核心线程的任务存放在阻塞队列的时候,如果当前队列中的任务已经存满了,我们也会判断其当前正在执行的核心线程是否过期超时,如果超时就会干掉核心线程所执行的任务,让队列中的任务可以继续被核心线程执行,然后待入队列的任务入队,此时我们使用的是阻塞队列,当队列满的时候,我们会进行阻塞而不是直接退出,因此可以保留当前待入队的任务的状态,当我们队列有空余位置的时候,自动唤醒,将任务入队。这就是我们之所以使用阻塞队列,而不使用非阻塞队列。 - 最大线程数和核心线程数的区别
最大线程数指的是可以创建的最大数量的线程,核心线程数指的是最大可以运行的线程数,核心线程数一定不能大于最大线程数,会报错。
核心线程数<=最大线程数 - 线程池中最大可存在的线程是:当核心线程数数量足够的时候,会先往队列中放,队列中的任务都是由核心线程去执行,队列如果满了,会判断当前线程数是否大于最大线程数,如果不大于最大线程数,则创建新的线程去执行任务。这么做的目的就是保证所有真正的线程都能够用到。
- 当我们阻塞队列 已经满了,但是总线程数小于最大的线程数的时候,我们会新建一个线程去执行,此时新建的线程也会如同之前的核心线程一样,一起被复用。
第二节 合理配置线程池
- 如何合理的配置线程池?
原则:
根据情况而定:CPU密集和IO密集
(1)CPU密集:任务需要大量的运算,就是run方法中有大量的运算,而没有阻塞情况,CPU可以全速的运行
(2)IO密集:IO操作多 ,执行任务的时候,需要大量IO操作,IO操作指的是请求,读取数据库,文件读取等;使用多线程技术,阻塞的时候调用其他线程执行其他任务,此时CPU也是全速运行,没有阻塞,不会浪费等待资源。
CPU密集和IO密集相反,CPU密集执行任务的时候,CPU运行的时候特别快。
IO密集型的时候线程数尽量配置多点。
规律:
(1)IO密集型多配置线程数,一般配置2*CPU核数(配置的是最大线程数,而不是核心线程数)
(2)CPU密集的情况下,配置的线程数就无需这么多,线程因为无需等待,CPU的运行速度快,所以线程数量一定小于IO密集型的。配置的线程数最好是等于CPU的核数(最大线程数)
总结:
(1)IO密集CPU会发生阻塞,配置的最大线程数是CPU当前核数*2
(2)CPU密集下不会阻塞,所以配置的最大线程数是当前电脑的CPU核数
第三节 异步请求模式
-
继承Thread类或者实现Runnable接口,run方法执行任务代码,但是run方法是没有返回值的,且无法抛出异常。子线程是否执行完,主线程若想感知较为麻烦。
-
而实现Callable接口,重写V call() 方法是可以有返回值的,且允许抛出异常。
-
儿子不能比他爹抛出更多的异常,这就是run方法无法抛异常(因为run方法在runnable接口的run方法定义中没有抛异常),但是call方法可以抛出异常(因为call方法在callable接口的call方法定义中抛出了异常)
-
future模式利用了wait和notify进行通信
-
future模式调用get方法之前和子线程中的代码是异步执行的,直到遇见get方法后才会进入阻塞,等待结果
-
Callable和Future来实现获取任务结果的操作。Callable用来执行任务产生结果,而Future用来获得结果。(Future的get方法得到结果)
- 举个简单的例子:
package com.xiyou.mayiThread.myexecutors.future;
import java.util.concurrent.*;
public class TestFutureCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
Future<String> future = cachedThreadPool.submit(new TaskCallable());
System.out.println("1. 主线程开始执行....");
String result = future.get();
System.out.println("2. 最后的结果是: " + result);
}
}
class TaskCallable implements Callable<String>{
public String call() throws Exception {
System.out.println("3. 正在执行任务,需要等待5s,执行任务开始");
Thread.sleep(5000);
System.out.println("4. 正在执行任务,需要等待5s,执行任务结束");
return "hello world";
}
}
执行结果:
1. 主线程开始执行....
3. 正在执行任务,需要等待5s,执行任务开始
4. 正在执行任务,需要等待5s,执行任务结束
2. 最后的结果是: hello world
分析上面的程序,观察其打印顺序,发现1,3先执行,等待5s后4,2再执行,为什么呢?因为Future的get是一个阻塞方法,只有等到了Callable的call接口返回了值以后,才会继续执行。
- Future常用方法(Future用来获取call方法的结果)
(1)V get():获取异步执行的结果,如果没有结果可用,此方法会阻塞到异步计算完成。
(2)V get(Long timeout, TimeUnit unit):获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。
(3)boolean isDone():如果任务执行结束,无论是正常结束或者是中途取消还是发生异常,都返回true。
(4)boolean isCanceller():如果任务完成前被取消,则返回true
(5)boolean cancel(boolean mayInterruptRunning):如果任务还没开始,执行cancel(…)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(…)方法将返回false。mayInterruptRunning参数表示是否中断执行中的线程。 - 通过方法分析我们也知道实际上Future上提供了3种功能,(1)能够中断执行中的任务。(2)判断任务是否执行完成。(3)获取任务执行完成后的结果。
第五节 future模式
- Future模式的核心在于:去除了主函数的等待时间,并使得原本需要等待的时间段可以用于处理其他业务逻辑。
- Future模式:对于多线程,如果线程A需要等待线程B的结果,那么线程A没有必要等待线程B执行完成,直至B有结果,我们可以先拿到一个未来的Future,等B有结果后再去其真实的结果。
- Future就相当于前端的AJAX技术。
- 下面我们模拟Future的实现,其就是依靠wait和notify进行实现的。
- 首先定义一个公共接口类,该类是用来获取线程的执行结果的
package com.xiyou.mayi.thread5.myFuture;
/**
* 公共的数据接口
*/
public interface Data {
/**
* 得到线程返回的数据
* @return
*/
public String getRequest();
}
- 首先定义一个模拟的请求,就是我们真正要执行的业务逻辑。比较耗时
package com.xiyou.mayi.thread5.myFuture;
/**
* 模拟网络请求,就是需要在多线程中执行的请求
*/
public class RealData implements Data{
// 用来记录返回的结果
private String result;
/**
* 构造函数
* @param data
*/
public RealData(String data){
System.out.println("正在使用的data: " + data);
try {
// 模拟网络请求传输慢
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行完毕...");
result = "Hello World";
}
@Override
public String getRequest() {
return result;
}
}
- 模拟future,实现future,若get的时候结果还没有准备好,调用wait,若准备好了(readData已经有对象,FLAG已经为true)唤醒线程,get恢复,继续执行。
package com.xiyou.mayi.thread5.myFuture;
public class FutureData implements Data {
// 标志位,用来判断是否已经获取到了数据
private volatile boolean FLAG;
// 该对象是模拟网络请求的,若执行完成,则会返回结果
private RealData realData;
/**
* 向其中赋值
* @param realData
*/
public synchronized void setRequest(RealData realData){
// 如果已经获取到了结果直接返回
if (FLAG) {
return;
}
// 如果没有获取到数据,传递对象
this.realData = realData;
FLAG = true;
// 通知线程,我已经处理完毕
notify();
}
/**
* 得到线程返回的数据
* @return
*/
@Override
public synchronized String getRequest() {
// 循环判断
while (!FLAG) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return realData.getRequest();
}
}
- 测试类:
其中模拟了execute方法,该方法的赋值是在子线程中,因为RealData的构造函数中模拟了耗时的请求,因此,主线程不应该等待其执行完再返回,应该直接返回该对象,调用对象的get方法,若模拟的耗时请求没有执行完,则调用的wait方法阻塞,到这里已经完全模拟了future的处理思路。
package com.xiyou.mayi.thread5.myFuture;
/**
* 实现自定义的Future
*/
public class MyFutureClient {
public Data execute(String queryStr) {
FutureData futureData = new FutureData();
// 用线程的方式赋值,主线程不应该等待。相当于是往线程池中添加任务。
new Thread(new Runnable() {
@Override
public void run() {
RealData realData = new RealData(queryStr);
futureData.setRequest(realData);
}
}).start();
// 不管赋值(这里是模拟网络请求)完成与否,直接返回,若没有完成get的时候会调用wait
return futureData;
}
}
class Test{
public static void main(String[] args) {
MyFutureClient futureClient = new MyFutureClient();
Data execute = futureClient.execute("请求");
System.out.println("请求发送成功");
// 模拟future的get方法,取不到值阻塞,阻塞用wait实现,有值了用notify唤醒
String result = execute.getRequest();
System.out.println("取到的结果是: " + result);
}
}
补充知识点:
Callable, Future, Runnable,FutureTask
一, Callable与Runnable
- Runnable是一个接口,他里面声明了一个run()方法
public interface Runnable{
public abstract void run();
}
由于run()方法返回值为void类型,所以执行完成任务之后无法返回结果。
- Callable位于java.util.concurrent包下,他也是一个接口,在他里面也只声明了一个方法,只不过这个方法叫做call();
public interface Callable<V>{
V call() throw Exception;
}
可以看到,这是一个泛型接口,call()函数返回的类型就是Callable的泛型V。
- 那么怎么使用Callable?一般情况下是配合ExecutorService来使用的(即与线程池结合使用),在ExecutorService接口中声明了若干个submit方法的重载版本
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
上面的方法中第一个submit方法里面的参数类型是Callable。一般情况下我们只使用第一个和第三个方法,第二个方法很少使用;
二, Future – 用来获取callable接口的方法
- Future是对于具体的Runnable或者Callable任务的执行结果进行取消,查询,是否完成,获取结果。必要时可以通过get方法来获取执行结果,该方法会阻塞直到任务返回结果。
- Future类位于java.util.concurrent包下,他是一个接口:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
上面已经介绍了具体的方法的含义。
三, FutureTask
- FutureTask的实现:
public class FutureTask<V> implements RunnableFuture<V>;
FutureTask类实现了RunnableFuture接口,我们再来看下RunnableFuture接口的实现:
public interface RunnableFuture<V> extends Runnable, Future<V>{
void run();
}
这里是一个多继承,因为两个类都是接口,所以可以多继承,这里可以看到RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
- FutureTask提供了两种构造
public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}
实际上,FutureTask是Future唯一的实现类
- FutureTask的使用方法:
- 线程池调用:
ExecutorService executor = Executors.newCachedThreadPool();
// MyTask是一个实现了Callable接口的类
MyTask myTask = new MyTask();
FutureTask<Integer> futureTask = new FutureTask<Integer>(myTask);
// 因为futureTask既可以看做是Runnable实现类,又可以看做是Future实现类,这里将其当做Runnable的实现类(Future不是Callable实现类)
executor.submit(futureTask);
- Thread直接调用
// MyTask是一个实现了Callable接口的类
MyTask myTask = new MyTask();
FutureTask<Integer> futureTask = new FutureTask<Integer>(myTask);
// 因为futureTask既可以看做是Runnable实现类,又可以看做是Future实现类,这里当做Runnable的实现类(是Future不是Callable实现类)
Thread thread = new Thread(futureTask);
thread.start();
总结:
- Callable是一个接口,实现其接口,重写call方法。该方法是线程方法,可以有返回值,可以跑出异常
- Future用来获取call方法的返回结果,判断其中的线程是否执行完成,由线程池对象的submit返回
- submit返回一个Future对象,execute不返回对象
- FutureTask可以看做Runnable和Future的实现类
- FutureTask是Future的唯一实现类
ListenableFuture
- ListenableFuture是一个接口,他从jdk的Future接口继承而来。添加了
void addListener(Runnable listener, Executor executor)
- ListenableFuture顾名思义就是可以监听的Future,他是对java原生Future的扩展增强。我们知道Future表示一个异步计算任务,当任务完成时可以得到计算的结果。如果我们希望一旦计算完成就拿到结果展示给其他用户或者做其他的计算,这时候我们就需要新建一个线程不停的等待结果返回,因为future的get方法是阻塞的,没有返回的时候会一直阻塞。这样做,代码比较复杂。使用ListenableFuture,可以帮助我们检测Future是否已经得到了结果,如果已经get到了结果,就自动调用其回调函数,这样做可以减少并发程序的复杂度
- 简单举个例子说明
(1)首先通过MoreExecutors类的静态方法listeningDecorator方法初始化一个ListeningExecutorService方法(可以把这一步看成线程池的Executors.newXXXXXX),然后使用该实例的submit方法即可初始化ListenableFuture(可以理解为线程池调用submit后返回的Future对象)。在submit中传递任务,表明其要执行的。
// 声明一个ListeningExecutorService对象,该对象可以理解为新建线程池后的返回的对象
ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
// 提交任务到线程池
final ListenableFuture<Integer> listenableFuture = listeningExecutorService.submit(new Callable<Integer>() {
public Integer call() throws Exception {
System.out.println("call execute...");
Thread.sleep(3000);
int i = 1/0;
return 8;
}
});
(2)我们用了两个方法来监听执行完成后的返回
方法一:
public void testListeneable(){
/**
* 添加监听器
* 第一个参数是监听之后执行的方法
* 第二个参数是需要监听的方法
*/
listenableFuture.addListener(new Runnable() {
// 执行成功后自动执行
public void run() {
try {
System.out.println("已经执行完毕了。。。结果是: " + listenableFuture.get());
// 关闭线程池
listeningExecutorService.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}, listeningExecutorService);
System.out.println("测试一下,这个是否是顺时执行的。。。");
}
方法二:
public void testListeneable2(){
/**
* 返回的监听
* 第一个参数是执行完之后返回的future对象
* 第二个参数是回调
*/
Futures.addCallback(listenableFuture, new FutureCallback<Integer>() {
// 执行成功,调用该方法
public void onSuccess(Integer result) {
System.out.println("执行成功......: " + result);
listeningExecutorService.shutdown();
}
// 执行失败调用该方法
public void onFailure(Throwable t) {
System.out.println("执行失败了.....");
System.out.println(t.getMessage());
listeningExecutorService.shutdown();
}
});
}
(3)测试
public static void main(String[] args) {
MyListenableFuture myListenableFuture = new MyListenableFuture();
myListenableFuture.testListeneable2();
System.out.println("主线程正常走");
}
这里肯定是推荐第二种方法的,因为第二种方法可以直接得到Future的返回值,或者处理错误情况。本质上第二种方法是通过第一种方法实现的,对其做了进一步的封装。另外ListenableFuture还有其他几项内置实现:
- SettableFuture:
- CheckedFuture
这里不做详细的解释。