|
多线程案例
1. 定时器
定时器, 类似于一个 "闹钟"
. 代码中的定时器, 通常都是设定 “多长时间之后, 执行某个动作”.
例如: 客户端发送请求之后, 就需要等待服务器响应. 如果服务器一直不响应, 客户端也不能一直死等下去. 客户端经常会设置一个超时时间, 这时就可以使用定时器来实现.
标准库中的定时器
-
标准库中提供了一个
Timer
类. Timer 类的核心方法为schedule
. -
schedule
包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
public class Test {
public static void main(String[] args) {
//标准库的计时器
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("7:00了,该起床了");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("9:00了,该起床了");
}
},5000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("8:00了,该起床了");
}
},4000);
System.out.println("闹钟响了!!");
}
}
这时我们运行代码, 我们发现, 按照时间执行完上述任务之后, 进程并没有退出. Timer
内部需要一组线程来执行注册的任务, 而这里的线程是 前台线程, 会影响进程退出.
定时器的实现
schedule
的第一个参数是一个任务, 我们需要能够描述这个任务. (任务包含两方面信息, 需要执行的工作, 与他开始执行的时间)
class MyTask implements Comparable<MyTask>{
//要执行的任务
private Runnable runnable;
//什么时间来执行任务(一个时间戳)
private long time;
public MyTask(Runnable runnable, long dalay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + dalay;
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
//实现 Comparable 接口, 便于下文 优先级队列的比较
@Override
public int compareTo(MyTask o) {
return (int)(this.time - o.time);
}
}
- 让
MyTimer
管理多个任务, 使用优先级阻塞队列 (保证每次取出等待的时间最短的任务, 与线程安全)
class MyTimer {
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable, long after) throws InterruptedException {
MyTask myTask = new MyTask(runnable, after);
queue.put(MyTask);
}
}
- 从队列中去元素, 创建一个单独的扫描线程, 让这个线程
不停的
来检查队首元素, 时间是否到了, 如果到了则执行该任务
class MyTimer {
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public MyTimer() {
// 一个扫描线程
Thread t = new Thread(() -> {
while (true) {
try {
//取出队首元素
MyTask task = queue.take();
//当前时间
long curTime = System.currentTimeMillis();
//假设当前时间是 大于等于 任务设定时间,就要执行任务了
if(curTime >= task.getTime()) {
task.getRunnable().run();
}else {
//没到时间, 再将 task 放回阻塞队列
queue.put(task);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
public void schedule(Runnable runnable, long after) throws InterruptedException {
MyTask myTask = new MyTask(runnable, after);
queue.put(myTask);
}
}
-
# 注意 #
当前的代码中存在一个严重的问题,while(true)
转的太快了, 造成了无意义的 CPU 浪费例如: 如果我们设定了一个任务, 这个任务是 8:00 开始, 现在的时间是 7:30, 在这半个小时里 while (true) 会每秒钟访问队首元素几万次. CPU 不断的在工作, 这里的等待是无意义的.
class MyTimer {
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
// 定义一个 locker 实例
Object locker = new Object();
public MyTimer() {
// 一个扫描线程
Thread t = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
MyTask task = queue.take();
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {
task.getRunnable().run();
} else {
queue.put(task);
//时间未到, 就等待 wait, 等待多久, 任务时间 - 当前时间
locker.wait(task.getTime() - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
public void schedule(Runnable runnable, long after) throws InterruptedException {
MyTask myTask = new MyTask(runnable, after);
queue.put(myTask);
//放入一个任务之后, 唤醒 wait 更新需要等待的时间
synchronized (locker) {
locker.notify();
}
}
}
完整代码 + 测试用例
//表示一个任务
class MyTask implements Comparable<MyTask>{
//要执行的任务
private Runnable runnable;
//什么时间来执行任务(一个时间戳)
private long time;
public MyTask(Runnable runnable, long dalay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + dalay;
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
return (int)(this.time - o.time);
}
}
class MyTimer {
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
Object locker = new Object();
public MyTimer() {
// 一个扫描线程
Thread t = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
//取出队首元素
MyTask task = queue.take();
//当前时间
long curTime = System.currentTimeMillis();
//假设当前时间是 大于等于 任务设定时间,就要执行任务了
if (curTime >= task.getTime()) {
task.getRunnable().run();
} else {
//没到时间, 再将 task 放回阻塞队列
queue.put(task);
locker.wait(task.getTime() - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
public void schedule(Runnable runnable, long after) throws InterruptedException {
MyTask myTask = new MyTask(runnable, after);
queue.put(myTask);
synchronized (locker) {
locker.notify();
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("7:00了 该起床了");
}
},3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("9:00了 该起床了");
}
},5000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("8:00了 该起床了");
}
},4000);
System.out.println("闹钟响了!!");
}
}
运行结果展示
2. 工厂模式
构造方法存在一定局限性, 为了绕开局限, 就引入了工厂模式
- 构造实例, 最主要的就是使用构造方法 new .
- 在 new 的过程中,就需要调用构造方法, 有时希望提供多种构造实例的方式
- 就需要重载构造方法, 来实现不同的版本的对象来创建
例如: 我们想通过两个函数分别求点的坐标
此处这两个版本的构造方法, 无法构成重载.
这时我们就可以用普通方法
, 代替构造方法. (使用普通方法, 在里面分别构造出 Point
对象, 再通过一些其他手段来进行设置)
public static Point makePointXY(double x, double y) {
Point p = new Point();
p.setX(x);
p.setY(y);
return p;
}
public static Point makePointRA(double r, double a) {
Point p = new Point();
p.setR(r);
p.setA(a);
return p;
}
我们将这种方法称为工厂方法.
3. 线程池
线程诞生的目的, 是因为进程的创建/ 销毁开销过大.
但如果线程创建的速率进一步频繁了. 此时线程创建/ 销毁的开销仍不能忽略, 这时我们就可以使用线程池来进一步优化这里的速度.
在一个池子中, 创建好许多线程. 当需要执行任务的时候, 就不需要重新创建线程了, 只需要直接从池子里取一个现成的线程, 直接使用. 用完, 也不必释放线程, 而是直接还回到线程池里.
线程池最大的好处就是减少每次启动、销毁线程的损耗
标准库中的线程池
-
创建线程池, 没有显式 new , 而是通过另外的
Executors
类的静态方法newCachedThreadPool
来完成, 这个方法就是工厂方法.在 Java 中, 线程池的本体叫做
ThreadPoolExecutor
.他的构造方法写起来非常麻烦, 为了简化构造,标准库就提供了一系列的工厂方法:- newFixedThreadPool: 创建固定线程数的线程池
- newCachedThreadPool: 创建线程数目动态增长的线程池.
- newSingleThreadExecutor: 创建只包含单个线程的线程池.
- newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
ExecutorService pool = Executors.newCachedThreadPool();
- 使用
.submit( )
添加任务, 里面只有一个Runnable
参数
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("一个任务");
}
});
}
线程池的实现
一个线程池可以同时提交 N 个任务, 对应的线程之中有 M 个线程来负责完成这 N 个任务
- 这时我们就需要使用一个
阻塞队列
, 把每个被提交的任务, 都放在阻塞队列中. 再搞 M 个线程来取队列元素.
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
- 核心操作为
submit
, 将任务加入线程池中
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
- 在构造方法中, 创建
M
个线程, 负责完成工作
public MyThreadPool(int m) {
//在构造方法中, 创建 M 个线程, 负责完成工作
for (int i = 0; i < m; i++) {
Thread t = new Thread(() -> {
while(true) {
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
完整代码 + 测试用例
class MyThreadPool {
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
public MyThreadPool(int m) {
//在构造方法中, 创建 M 个线程, 负责完成工作
for (int i = 0; i < m; i++) {
Thread t = new Thread(() -> {
while(true) {
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool = new MyThreadPool(10);
for (int i = 0; i < 100; i++) {
int taskId = i;
pool.submit(new Runnable() {
@Override
public void run() {
//变量捕获只能捕获一个final类型变量, 或者是一个实际final变量
System.out.println("执行当前任务 " + taskId + " 当前线程: " + Thread.currentThread().getName());
}
});
}
}
}
运行结果展示
线程池的本体 ThreadPoolExecutor 的构造方法.
|
以上就是今天要讲的内容了,希望对大家有所帮助,如果有问题欢迎评论指出,会积极改正!!