JUC基础知识(二)
一:Callable、Future接口
下面截取JDK源码中 Thread类的注释部分。
目前我们学习了有两种创建线程的方法
一种是通过继承 Thread 类,另一种是通过实现 Runnable 接口 创建线程。(这两种方式是最基本的,其他的Callable接口和线程池其实还是基于这两种方式实现的)
但是,Runnable 缺少的一项功能是,当线程终止时(即 run() 完成时),我们无法使线程返回结果。为了支持此功能,Java 中提供了 Callable 接口。
Callable 接口源码:
Callable接口,只是用于单纯地去获得返回值,但是什么时候去获取,或者查看线程运行的状态,就需要借助Future
Future的源码如下:
public interface Future<V> {
//取消任务的执行,任务已经完成或者已经被取消时,调用此方法,会返回false
boolean cancel(boolean mayInterruptIfRunning);
//判断任务是否被取消
boolean isCancelled();
//判断任务是否完成(正常完成、异常以及被取消,都将返回true)
boolean isDone();
//阻塞地获取任务执行的结果
V get() throws InterruptedException, ExecutionException;
//在一定时间内,阻塞地获取任务执行的结果
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
可以看得出来,Future提供了取消任务、获取任务执行的状态与获取结果的能力。
类图关系:
可以看出来 FutureTask中的构造方法可以传递Callable,而且Runnable接口有实现类FutureTask,所以说FutureTask是把Callable和Runnable接口联系在了一起。所以说其实使用Callable接口创建线程其实本质上还是实现Runnable,只不过使用FutureTask联系这两者能够方便的获取到返回值。这也就照应了上面说的创建线程有两种方式。1:继承Thread类。2:实现Runnable接口。
使用Callable接口创建线程:
List<Integer> list = new ArrayList<>();
//Lambda表达式
FutureTask<Object> futureTask = new FutureTask<>(()->{
list.add(1);
list.add(2);
list.add(3);
return list;
});
//创建一个线程
new Thread(futureTask).start();
System.out.println(futureTask.get());
二:Java中的线程池
1、实现机制
主线程使用ececute() 方法,向线程池中提交任务。
①首先会先判断当前线程数是否超过了core Pool中规定的最大线程数,如果未超出,就会直接在core Pool中创建一个新的线程。②如果已经超出,创建线程的任务会进入阻塞队列BlockingQueue中进行等待。③如果core Pool和BlockingQueue都已经满了,则就会在max Pool中创建新的线程。④如果连max Pool中都满了,那么线程池就会采取一种饱和策略来解决问题(饱和策略在下面线程池的创建中会有介绍)。
2、线程池的创建
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds, runnableTaskQueue, handler);
创建一个线程池时需要输入几个参数,如下:(七个核心参数)
1)corePoolSize(线程池的基本大小)。
2)maximumPoolSize(线程池最大数量)。
3)keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。
4)TimeUnit(线程活动保持时间的单位)。
5)runnableTaskQueue(任务队列)。
① ArrayBlockingQueue
② LinkedBlockingQueue
③ SynchronousQueue
④ PriorityBlockingQueue
6)ThreadFactory(线程工厂。用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字)。
7)RejectedExecutionHandler(四种饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。
①AbortPolicy:直接抛出异常(默认)。
②CallerRunsPolicy:只用调用者所在线程来运行任务。
③DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
④DiscardPolicy:不处理,丢弃掉。
3、向线程池提交任务
execute()
execute 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit()
submit 方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个对象可以判断任务是否执行成功,并且可以通过future的get 方法来获取返回值。
4、线程池的生命周期
可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。
1、线程池的五种状态,只能由小到大迁移,即-1 > 0 > 1 > 2 > 3 。
2、shutdown()不清空任务队列、会等它们完成, shutdownNow()会清空任务队列、不等它们完成。shutdown()只中断空闲的线程, shutdownNow()会中断所有的线程。
3、TIDYING 和 TREMINATED二者之间执行了一个钩子函数 terminated(),目前这是一个空的实现(自己可以去实现自己的业务需求)。
三:理解同步异步与阻塞非阻塞
同步:执行一个操作之后,等待结果,然后才继续执行后续的操作。
异步:执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操作。
阻塞:进程给CPU传达一个任务之后,一直等待CPU处理完成,然后才执行后面的操作。
非阻塞:进程给CPU传达任务后,继续处理后续的操作,隔断时间再来询问之前的操作是否完成。这样的过程其实也叫轮询。
下面举个段子说明一下
同步异步与阻塞非阻塞(段子)
老张爱喝茶,废话不说,煮开水。
出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1、老张把水壶放到火上,立等水开。(同步阻塞)
老张觉得自己有点傻
2、老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
3、老张把响水壶放到火上,立等水开。(异步阻塞)
老张觉得这样傻等意义不大
4、老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)老张觉得自己聪明了。
所谓同步异步,只是对于水壶而言。普通水壶,同步;响水壶,异步。
虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。
立等的老张,阻塞;看电视的老张,非阻塞。
四:阻塞队列
详情介绍在下面的两篇文章:
生产者消费者模型:https://blog.csdn.net/A12115419/article/details/120152178
阻塞队列:
https://blog.csdn.net/A12115419/article/details/119760828