目录
一 . 定时器
1) 实现一个定时器
二 . 线程池
三 . 线程池的实现
定时器就像一个闹钟 , 进行定时 , 在一定时间后,被唤醒并执行某个之前设定好的任务.
Java标准库中提供的定时器 , 在 java.util 包下的 Timer 类
import java.util.Timer;
import java.util.TimerTask;
public class Demo4 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Hello");
}
}, 2000);
}
}
schedule : 安排
定时器的内部管理着很多任务 , 每个任务的触发时间是不同的 , 通过一个 / 一组 工作线程, 每次都找到这些任务中最先到到达时间的任务~~
一个线程 , 先执行最早的任务 , 然后再去执行第二早的 , .......
如果任务的时间到了 , 就执行 , 没到就等
定时器 , 可能有多个线程在执行schedule方法 , 因此在多线程的情况下要保证线程安全 ,
使用优先级队列来管理任务.
首先 建造一个MyTask 要包含要执行的任务 , 时间 .
package Demo5;
import java.util.concurrent.PriorityBlockingQueue;
// 表示一个任务
class MyTask implements Comparable{
// 使用Runnable来表述一个任务
public Runnable runnable;
// 为了方便后序判定 , 这里使用绝对的时间戳
public long time;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
// 绝对时间戳 = 当前的时间戳 + delay , 作为该任务的实际执行的时间戳
this.time = System.currentTimeMillis() + delay;
}
@Override
public int compareTo(MyTask o) {
return (int)(this.time - o.time);
}
}
public class MyTimer {
// 使用优先级阻塞队列 即实现了线程安全 并且 还可以使线程处于阻塞状态
private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();
// 提供schedule 方法 用来安排任务 和 时间
public void schedule(Runnable runnable, long delay) {
MyTask myTask = new MyTask(runnable,delay);
// 放入队列中
queue.put(myTask);
}
public MyTimer() {
Thread t = new Thread(() -> {
while (true) {
try {
MyTask task = queue.take();
if (task.time <= System.currentTimeMillis()) {
// 时间到了 可以执行任务了
task.runnable.run();
} else {
// 如果时间还没到 再把取出的任务放回队列
queue.put(task);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
package Demo5;
public class Test {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("1");
}
},2000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("2");
}
},1000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("3");
}
},100);
}
}
解决方案 :
可以基于wait 这样的机制来实现~~
wait 有一个版本 , 指定等待时间 , (不需要notify 唤醒 , 时间到了自然就醒了)
只需要计算出当前时间和任务目标之间的时间差 , 就等待这么长时间就可以了.
对此 , 我们就发现wait 和 sleep 都可以指定一段时间 , 为啥不用sleep呢 ?
原因在于sleep 不能中途唤醒 , 而 wait 可以被notify 唤醒.
例如 : 在我们等待的过程中 , 又有一个新的任务放入了队列中 , 新的任务可能被执行的时间是最前面的 , 但是此时我们等待的还是之前的任务, 因此每当我们加入一个新的任务时 , 就要通过notify来唤醒wait.
在代码中改变这两句 , 就可以解决忙等问题!
synchronized (locker) {
locker.notify();
}
locker.wait( task.time - System.currentTimeMillis());
重点注意加锁的位置 , 不要让notify "空打一炮".
整体代码 :
package Demo5;
import java.util.concurrent.PriorityBlockingQueue;
// 表示一个任务
class MyTask implements Comparable{
// 使用Runnable来表述一个任务
public Runnable runnable;
// 为了方便后序判定 , 这里使用绝对的时间戳
public long time;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
// 绝对时间戳 = 当前的时间戳 + delay , 作为该任务的实际执行的时间戳
this.time = System.currentTimeMillis() + delay;
}
@Override
public int compareTo(MyTask o) {
return (int)(this.time - o.time);
}
}
public class MyTimer {
// 使用优先级阻塞队列 即实现了线程安全 并且 还可以使线程处于阻塞状态
private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();
// 提供schedule 方法 用来安排任务 和 时间
public void schedule(Runnable runnable, long delay) {
MyTask myTask = new MyTask(runnable,delay);
// 放入队列中
queue.put(myTask);
synchronized (this) {
this.notify();
}
}
public MyTimer() {
Thread t = new Thread(() -> {
synchronized (this) {
while (true) {
try {
MyTask task = queue.take();
if (task.time <= System.currentTimeMillis()) {
// 时间到了 可以执行任务了
task.runnable.run();
} else {
// 如果时间还没到 再把取出的任务放回队列
queue.put(task);
this.wait( task.time - System.currentTimeMillis());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
}
}
池的目的就是提高效率 .
线程池 : 提前把线程准备好 , 创建线程不再是直接从系统申请, 而是从池子里拿.
线程用完后 , 再放回池子.
进程 比较重 , 频繁创建销毁 , 开销大 , 解决方案就是 : 进程池 或者 线程
线程 虽然比进程轻了 , 但是如果频繁的创建和销毁 , 开销仍然还是存在的 , 解决方法 : 线程池 或者 协程.
为什么线程放在池子里 , 就比从系统这边申请来的更快呢?
我们自己写的代码 , 称为"用户态"运行的代码
而其中有一些代码 需要调用操作系统的API , 进一步的逻辑会在内核中执行 .
在内核中运行的代码 , 称为"内核态"运行的代码.
创建线程 , 本身就需要内核的支持 (创建线程本质上就是在内核中搞个PCB , 加到链表中)
而把创建好的线程放到池子里 , 这个池子是用户态自己实现的 , 线程从池子中取 / 往池子中放 都是用户态自己实现的 , 在这个过程中不需要涉及到内核态 , 就是纯粹的用户态代码就可以完成 .
举个栗子 :
好比让你的朋友给你从外面带个东西 , 和你自己去商店买.
让别人带这个时间是不可控的 , 带回来不知道是什么时候了.(可能10分钟 或者1个小时, 两个小时 ,)
而如果自己去买这个时间是可控的.
总结 : 纯用户态操作 , 时间是可控的 .
涉及到内核态操作, 时间就太不可控了.
Java 标准库中的线程池 : ThreadPoolExecutor
标准库中提供了一个简化版本的线程池 :
Executors : 本质上是针对ThreadPoolExecutor 进行了封装 , 提供了一些默认参数.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class Demo6 {
public static void main(String[] args) {
// 创建一个固定线程数目的线程池 , 参数指定了线程的个数
ExecutorService pool = Executors.newFixedThreadPool(10);
// 创建一个自动扩容的线程池 , 会根据任务量来自动进行扩容
//Executors.newCachedThreadPool();
// 创建一个只有一个线程的线程池
//Executors.newSingleThreadExecutor();
// 创建一个带有定时器功能的线程池 , 类似于Timer
//Executors.newScheduledThreadPool(10);
for (int i = 0; i < 100; i++) {
int num = i;
pool.submit(() -> {
System.out.println(num);
});
}
}
}
附加 : 虽然线程池的参数这么多 , 但是使用的时候最最重要的还是核心参数的设定最重要!
正确的做法 : 要通过性能测试的方法 , 找到一个最合适的值.
例如 : 写一个服务器程序 , 服务器里通过线程池 , 多线程池来处理用户请求.
根据实际的场景 , 根据测试用户的请求量 (500/1000/2000) , 构造一个合适的值 .
当线程数多了 , 整体的速度是会变快 , 但是CPU占用率会高(占用率太高 如果突然来一波请求的峰值 此时服务器可能就挂了)
当多线程少了 , 整体的速度是会变慢 , 但是CPU占用率会下降.
需要做的是从上述两个中取到一个中间值. 通过性能测试找到那个平衡点.
标准库提供的四种拒绝策略 :
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
class MyThreadPool {
// 1. 描述一个任务 , 直接使用Runnable
// 2. 使用阻塞队列来组织任务
BlockingQueue queue = new LinkedBlockingDeque<>();
// 3. 描述一个线程 , 工作线程的功能就是从任务队列中取任务并执行的.
static class Worker extends Thread {
BlockingQueue queue = null;
public Worker(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 4. 创建List来组织若干个线程
private List workers = new ArrayList<>();
// 构造方法
public MyThreadPool(int n) {
for (int i = 0; i < n; i++) {
Worker worker = new Worker(queue);
worker.start();
workers.add(worker);
}
}
// 5. 创建一个方法 , 安排任务
public void submit(Runnable runnable) {
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Demo7 {
public static void main(String[] args) {
MyThreadPool myThreadPool = new MyThreadPool(10);
for (int i = 0; i < 100; i++) {
int num = i;
myThreadPool.submit(() -> {
System.out.println(num +"执行线程: "+ Thread.currentThread().getName());
});
}
}
}
将任务分给了10个线程 , 每个线程都去执行一部分任务.