三、定时器
在 Java 标准库中,为我们提供了 Timer 这个类,作为定时器。
Timer 类的核心方法为 schedule( )
schedule 包含两个参数:
第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行( 单位为毫秒 )
程序清单8:
import java.util.Timer; import java.util.TimerTask; public class Test { public static void main(String[] args) { System.out.println("代码开始执行..."); Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("触发定时器"); } },3000); } }
输出结果: 3秒后触发定时器
实现定时器
程序清单9:
import java.util.concurrent.PriorityBlockingQueue; public class Test { //使用 Task 这个类来描述一个任务 static class Task implements Comparable<Task>{ //command 表示这个任务是什么 private Runnable command; //time 表示这个任务什么时候到时间,它用毫秒级的时间戳来表示 private long time; //约定参数 time 是一个时间差(类似于 3000) //希望 this.time 来保存一个绝对的时间 (毫秒级时间戳) public Task (Runnable command, long time) { this.command = command; //传入的时间再 + 时间戳 this.time = System.currentTimeMillis() + time; } public void run() { command.run(); } @Override public int compareTo(Task o) { return (int) (this.time - o.time); } } static class Timer { //使用优先级阻塞队列来组织这些任务 private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>(); //使用 locker 对象来解决忙等的问题 private Object locker = new Object(); public void schedule (Runnable command, long delay) { Task task = new Task(command,delay); queue.put(task); //每次插入新的任务都要唤醒扫描线程,让扫描线程能够重新计算 wait 时间,保证新的任务也不会错过 synchronized (locker) { locker.notify(); } } public Timer() { //创建一个扫描线程,这个扫描线程就是用来判定当前任务的,看看是不是已经到时间能执行了 Thread t = new Thread() { @Override public void run() { while (true) { //取出队列的首元素,判定时间是不是到了 try { Task task = queue.take(); long curTime = System.currentTimeMillis(); if (task.time > curTime) { //假设任务时间 9点,当前时间 8点,说明时间还未到 queue.put(task); synchronized (locker) { locker.wait(task.time - curTime); } } else { task.run(); } } catch (InterruptedException e) { e.printStackTrace(); break; } } } }; t.start(); } } public static void main(String[] args) { System.out.println("代码开始执行..."); Timer timer = new Timer(); timer.schedule(new Runnable() { @Override public void run() { System.out.println("触发定时器"); } },3000); } }
输出结果:
四、线程池(经典面试题)
引言
我们都知道多进程就是用来解决并发编程的方案,但进程的创建和销毁,开销比较大。
因此引入了线程,线程比进程要轻量很多。即便如此,在某些场景中,需要频繁地创建并销毁线程,而此时,线程的创建和销毁操作,相对来说也是有一定开销。那么有没有什么办法能够使线程调度优化变得更好一些呢?
1. 线程池的概念
那么就引入线程池这样的概念,现在我们将一个线程想象成一个东西,将线程池想象成一个储备池。而在某个场景需要使用线程的时候,我们就直接从池中取一个线程出来即可,而这个线程池并不是临时创建的,而是在某个任务需求之前就创建出来了这个池子。那么,当我们不需要某个线程的时候,就把线程还回池子中,此时,关于这样线程池的操作,比线程的创建和销毁的效率更高。
这就好比于你投简历之后面试,当你投的这家公司,某个团队需要10人,而有50人竞争这个岗位,那么,如果你通过了面试,就能直接入职。
假设你没有通过岗位,它会将你放入一个 "人才储备库 ",有一天,公司比较重视这个团队的一个业务,但这个业务人手又不够,却又比较赶工,那么原先需要10人的岗位,现在需要20人。那么,多出来需要招聘10人的这个岗位,就可以从 "人才储备库 "中挑选。
2. 线程池的好处
创建和销毁线程,本质上需要将用户态切换到内核态,然后再对内核态的 PCB 进行操作。如果使用线程池这个手段,我们拿放线程都只在用户态,不涉及对 PCB 的修改,在针对频繁的线程操作的场景下,显然,线程池能够带能更高效的操作。
3. 内核态和用户态
用户态就是应用程序执行的代码,内核态就是操作系统执行的代码,一般来说,两者之间的切换,是一个开销较大的过程。
而内核是很忙的,要给许许多多个进程同时提供服务,因此,一旦要将某个进程把某个任务交给内核来去做,此时什么时候能把事情做好,有些难以把握了。
这就像买高铁票一样,如果我们去高铁站的人工服务窗口购票,显然是很麻烦的一件事情,第一,你要排队,第二,售票员询问你乘坐的火车类型、车次、时间…,第三,售票员要为很多人服务,所以在面对形形色色的业务,他们所执行的时间较慢。而如果你直接从网上购票就不一样了,你可以按照自己的需求,很简单方便地就选购好了,退票、换票等等也比较直接。繁忙的售票员与你购票的场景就相当于内核态和用户态交互的过程,而你自己通过网上购票的场景就是用户态自己实现的过程,除了使用手机,就是自己与自己交互的过程,而此时的购票用手机就相当于一个池子。
4. ThreadPoolExecutor 类
在 Java 标准库中,提供了现成的线程池组件,即 ThreadPoolExecutor 类。但这个类使用起来较为复杂,需要传入很多参数。
我们一起来看看一个其中参数最多的构造方法:
ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory , RejectedExecutionHandler handler )
5. ThreadPoolExecutor 类中构造方法中的参数是什么意思(面试题)
ThreadPoolExecutor 类中,包含的线程的数量并不是一成不变的。
它能够根据任务量来自适应,如果任务较多,就会多创建一些线程;
如果任务较少,就会少创建一些线程。
(1) corePoolSize:核心线程数
(2) maximumPoolSize:最大线程数
我们可以将线程池想象成是一个 " 公司 ",公司里面的每个员工,就相当于是一个线程。
corePoolSize 就相当于正式工的数量,maximumPoolSize 就相当于正式工+临时工
员工分成两类:
① 正式工:签了劳动合同的,不能随便辞退。正式员工允许摸鱼 ( 这样的线程即使是空闲,也不会被销毁 )
② 临时工:没签劳动合同,可以随时辞退。临时工不允许摸鱼 ( 如果临时工线程摸鱼的时间达到一定的程度了,就会被销毁 )
如果我们要解决的任务场景对应的任务量比较稳定的,就可以设置 corePoolSize 和 maximumPoolSize 尽量接近 (临时工就可以尽量少一些)
如果要解决的任务场景,任务量波动比较大,就可以设置 corePoolSize 和 maximumPoolSize相差更大一些 (临时工就可以多一些)
(3) keepAliveTime
这就相当于临时工摸鱼可以摸多长时间
(4) unit
keepAliveTime 的时间单位
(5) workQueue
阻塞队列,组织了线程池要执行的任务。
(6) threadFactory
线程的创建方式,通过这个参数,来设定不同线程的创建方式。
Factory 就是工厂的意思。
(7) RejectedExecutionHandler handler
拒绝策略,当任务队列满的时候,又来了新任务,这个参数就是来处理这件事情。他根据你的需求可能会做出如下决定:
丢弃最新的任务,丢弃最老的任务,阻塞等待,抛出异常…
6. Executors 类
由于 ThreadPoolExecutor 类使用起来比较复杂,所以 Java 标准库又提供了一组其他的类,相当于对ThreadPoolExecutor 又进行了一层封装,即 Executors 这个类。
这个类相当于一个 " 工厂类 ",我们通过这个类提供的一组工厂方法,就可以创建出不同风格的线程池实例了。
(1) newFixedThreadPool:创建出一个固定线程数量的线程池 ( 完全没有临时工的版本 )
(2) newCachedThreadPool:创建出一个数量可变的线程池 ( 完全没有正式员工, 全是临时工 )
(3) newSingle’ ThreadPool:创建出一个只包含一个线程的线程池 ( 只是特定场景下使用 )
(4) newScheduleThreadPool:能够设定延时时间的线程池 ( 插入的任务能够过一会再执行 ),相当于进阶版的定时器。
程序清单10:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Test { public static void main(String[] args) { //1. 创建出一个线程数量为 10 的线程池的实例 ExecutorService service = Executors.newFixedThreadPool(10); //2. 给这个实例中加 1 个任务 service.submit(new Runnable() { @Override public void run() { System.out.println("hello"); } }); } }
输出结果:
程序清单11:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Test { public static void main(String[] args) { //1. 创建出一个线程数量为 10 的线程池的实例 ExecutorService service = Executors.newFixedThreadPool(10); //2. 给这个实例中加 20 个任务 for (int i = 0; i < 20; i++) { service.submit(new Runnable() { @Override public void run() { System.out.println("hello"); } }); } } }
输出结果:最后处于阻塞等待状态。
我们在程序开头时,设置了当前线程池的线程数量为为 10个 工作线程,我们往任务队列中加入了 20个 任务,此时这 10个 工作线程就会从任务队列中,先取出 10个 任务,然后并发执行这些任务。之后,这些线程谁执行完了当前的任务,谁就去任务队列中重新取一个新的任务。直到把线程池任务队列中的任务都取完了,此时线程池的工作线程就阻塞等待,即等待着新任务的到来。
五、工厂模式
工厂模式存在的意义,就是在给构造方法填坑。
工厂方法:工厂方法其实就是普通的方法,方法里面会调用对应的构造方法。并进行一些初始化操作,最后返回这个对象的实例。
举个例子:假设有个 Point类,表示平面上第一个点,我们希望通过笛卡尔坐标和极坐标这两种方式来构造这个点。我们通过构造方法来试一试这个操作。代码如下,我们发现,IDEA 编译器出现编译时错误,说两个 double 参数的构造方法已经存在。也就是说,我们使用传统的构造方法,需要满足方法之间的重载。
① 方法名相同
② 方法的参数个数不同 / 方法的参数类型不同
③ 方法的返回值类型不作要求
解决上面的这个问题:
我们不使用构造方法来构造实例了,而是使用其他的方法来进行构造实例。这样用来构造实例的方法,就称为 " 工厂方法 ",顾名思义,工厂就是用来制造的!
拓展:工厂模式实际上就是一种设计模式,而设计模式就是与语言相关的,有些语言中存在一些语法缺陷,所以在某些情况下,就需要依赖设计模式来进行约束。
实现简单版本的线程池
步骤:
(1) 创建一个线程池类 Thread
(2) 使用一个阻塞队列组织若干个任务,将任务用 Runnable 表示
(3) 需要创建一些工作线程
程序清单12:
import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class Test1 { static class Worker extends Thread { private BlockingQueue<Runnable> queue = null; public Worker (BlockingQueue<Runnable> queue) { this.queue = queue; } /** * 工作线程的具体逻辑,需要从阻塞队列中取任务 */ @Override public void run() { while (true) { try { Runnable command = queue.take(); //通过 run 方法来执行这个任务 command.run(); } catch (InterruptedException e) { e.printStackTrace(); } } } } static class ThreadPool { //包含一个阻塞队列,用来组织任务 private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(); //将当前的工作线程都存放在顺序表中 private List<Thread> workers = new ArrayList<>(); private static final int MAX_WORKER_COUNT = 10; /** * submit 方法把任务存放到当前线程池中 * submit 方法不光可以将任务放到阻塞队列中,也可以负责创建线程 */ public void submit (Runnable command) throws InterruptedException { if (workers.size() < MAX_WORKER_COUNT) { //如果当前工作线程的数量未达到线程数目的上限,就创建出新的线程 //工作线程就专门通过一个类来完成 Worker worker = new Worker(queue); worker.start(); workers.add(worker); } queue.put(command); } } public static void main(String[] args) throws InterruptedException { ThreadPool pool = new ThreadPool(); for (int i = 0; i < 20; i++) { pool.submit(new Runnable() { @Override public void run() { System.out.println("hello"); } }); } } }
输出结果: