宁晋网站建设设计,nas服务器 做网站,做网站用什么前端框架,开发做网站公司多线程案例
1、案例一#xff1a;线程安全的单例模式
单例模式
单例模式是设计模式的一种 什么是设计模式#xff1f; 设计模式好比象棋中的 “棋谱”#xff0c;红方当头炮#xff0c;黑方马来跳#xff0c;针对红方的一些走法#xff0c;黑方应招的时候有一些固定的…多线程案例
1、案例一线程安全的单例模式
单例模式
单例模式是设计模式的一种 什么是设计模式 设计模式好比象棋中的 “棋谱”红方当头炮黑方马来跳针对红方的一些走法黑方应招的时候有一些固定的套路按照套路来走局势就不会吃亏也就发明了一组棋谱称为设计模式 软件开发中也有很多常见的 “问题场景”针对一些典型的场景给出了一些典型的解决方案 有两个设计模式是非常常见的 其一是单例模式其二是工厂模式
单例模式 单个 实例 (对象)
在有些场景中有的特定的类只能创建出一个实例不应该创建多个实例
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例这种单例模式在实际开发中是非常常见也非常有用的开发中的很多 “概念” 天然就是单例JDBCDataSource这样的对象就应该是单例的 Java 里实现单例模式的方式有很多单例模式的两种典型实现 饿汉模式懒汉模式 举例洗碗 1.中午这顿饭使用了4个碗吃完之后立即把这4个碗给洗了~[饿汉] ⒉中午这顿饭使用了4个碗吃完之后先不洗。晚上这顿只需要2个碗然后就只洗2个即可~~[懒汉]
第二种是更加高效的操作—般是褒义词 (在计算机中提高效率)
饿汉的单例模式是比较着急地去进行创建实例 懒汉的单例模式是不太着急地去创建实例只是在用的时候才真正创建 1.1、饿汉模式
private static Singleton instance;注意 类里面使用 static 修饰的成员应该叫做 “类成员” “类属性 / 类方法”相当于这个属性对应的内存空间在类对象里面 不加 static 修饰的成员叫做 “实例成员” “实例属性 / 实例方法” 静态变量 属于类存储在方法区随着的类加载而加载 成员变量 属于对象存储在堆中随着对象的创建而创建 static 是让当前 instance 属性是类属性了 一个类对象在一个 Java 进程中是唯一实例的 (JVM保证的)类属性是长在类对象上的进一步的也就保证了类的 static 成员也是只有一份的 类对象 ! 对象 类就相当于实例的模板基于模板可以创建出很多的对象来 对象(实例) java 代码中的每个类都会在编译完成后得到 .class文件类对象就是 .class 文件 JVM 运行时就会加载这个 .class 文件读取其中的二进制指令并解析在内存中构造出对应的类对象 (类加载)形如 Singleton.class)类对象里就有 .class 文件中的一切信息 包括类名是啥类里有哪些属性每个属性叫啥名字每个属性叫啥类型每个属性是 public private… 基于这些信息才能实现反射
// 通过 Singleton 这个类来实现单例模式保证 Singleton 这个类只有唯一实例
class Singleton {// 1使用 static 创建一个实例,并且立即进行实例化// 这个 instance 对应的实例,就是该类的唯一实例private static Singleton instance new Singleton();// 2提供一个方法让外面能够拿到唯一实例public static Singleton getInstance() {return instance;}// 3为了防止程序猿在其他地方不小心地 new 这个 Singleton就可以把构造方法设为 private// 把构造方法设为 private在类外面就无法通过 new的方式来创建这个 Singleton实例了!private Singleton() {};
}public class demo1 {public static void main(String[] args) {Singleton instance Singleton.getInstance();Singleton instance2 Singleton.getInstance();System.out.println(instance instance2); // true 两个引用相同}
}针对这个唯一实例的初始化比较着急类加载阶段就会直接创建实例 (程序中用到了这个类就会立即加载)
饿汉模式中 getlnstance仅仅是读取了变量的内容 如果多个线程只是读同一个变量不修改此时仍然是线程安全的 1.2、懒汉模式 - 单线程
class Singleton2 {// 1就不是立即就初始化实例.private static Singleton2 instance null;// 2把构造方法设为 privateprivate Singleton2() {}// 3提供一个方法来获取到上述单例的实例// 只有当真正需要用到这个实例的时候才会真正去创建这个实例public static Singleton2 getInstance() {if (instance null) {instance new Singleton2();}return instance;}
}只有在真正使用到 getInstance 的时候才会真的创建实例
一个典型的案例 notepad 这样的程序在打开大文件的时候是很慢的 (你要打开一个1G大小的文件此时 notepad 就会尝试把这 1G 的所有内容都读到内存中) [饿汉] 像一些其他的程序在打开大文件的时候就有优化 (要打开 1G 的文件但是只先加载这—个屏幕中能显示出来的部分) [懒汉] 1.3、懒汉模式 - 线程安全
真正要解决的问题是实现一个线程安全的单例模式 线程安全不安全具体指的是多线程环境下并发的调用 getInstance 方法是否可能存在 bug
——懒汉模式 与 饿汉模式 在多线程环境下是否线程安全 饿汉模式这里多线程调用只是涉及到读操作 懒汉模式中包含读操作和修改操作存在线程安全问题 上述罗列出了一种可能的排序情况实际情况是有很多种 通过上述分析就可以看出当前这个代码中是存在bug可能导致实例被创建出多份来
如何保证懒汉模式的线程安全呢加锁
可不是说代码中有了 synchronized 就—定线程安全synchronized 加的位置也得正确不能随便写
本质是读比较写这三个操作不是原子的。这就导致了 t2 读到的值可能是 t1 还没来得及写的(脏读)导致多次 new所以要把锁加在外面此时才能保证 读操作 和 修改操作 是一个整体 使用这里的类对象作为锁对象 (类对象在一个程序中只有唯——份就能保证多个线程调用 getInstance 的时候都是针对同一个对象进行的加锁)
public static Singleton2 getInstance() {synchronized (Singleton.class) { // 类对象作为锁对象if (instance null) {instance new Singleton2();}}return instance;
}1.4、懒汉模式 - 锁竞争
当前虽然加锁之后线程安全问题得到解决了但是又有了新的问题
对于刚才这个懒汉模式的代码来说 线程不安全是发生在 instance 被初始化之前的未初始化的时候多线程调用 getinstance就可能同时涉及到读和修改但是一旦 instance 被初始化之后后续调用 getlnstance此时 instance 的值一定是非空的if 判断不成立也就线程安全了因此就会直接触发 returngetlnstance 就只剩下两个读操作相当于一个是比较操作一个是返回操作这两个都是读操作
而按照上述的加锁方式无论代码是初始化之后还是初始化之前。加锁是有开销的每次调用 getinstance 都会进行加锁也就意味着即使是初始化之后 (已经线程安全了)但是仍然存在大量的锁竞争 加锁确实能让代码保证线程安全也付出了代价 (程序的速度就慢了)
所以为啥不推荐使用 vector hashtable ?? 就是因为这俩类里面就是在无脑加锁
改进方案在加锁这里再加上一层条件判定即可对象还没创建才进行加锁对象创建过了就不再加锁了 条件就是当前是否已经初始化完成 (instance null)
class Singleton2 {// 1就不是立即就初始化实例.private static Singleton2 instance null;// 2把构造方法设为 privateprivate Singleton2() {}// 3提供一个方法来获取到上述单例的实例// 只有当真正需要用到这个实例的时候才会真正去创建这个实例public static Singleton2 getInstance() {if (instance null) {synchronized (Singleton.class) { // 类对象作为锁对象if (instance null) {instance new Singleton2();}}}return instance;}
}这俩条件—模一样只是一个美丽的巧合而已这俩条件起到的效果 / 预期的目的是完全不—样的 上面的条件判定的是是否要加锁 下面的条件判定的是是否要创建实例 碰巧这两个目的都是判定 instance 是否为 null
在这个代码中看起来两个—样的 if 条件是相邻的但是实际上这两个条件的执行时机是差别很大的 加锁可能导致线程阻塞当执行到锁结束之后执行到第二个 if 的时候第二个 if 和第一个 if 之间可能已经隔了很久的时间沧海桑田。程序的运行内部的状态这些变量的值都可能已经发生很大改变了。 如外层条件是 10:16 执行的里层条件可能是 10:30 执行的此时 instance 可能已经被其他线程给修改了。
如果去掉了里层的 if 就变成了刚才那个典型的错误代码加锁没有把读修改这操作进行打包 public static Singleton2 getInstance() {if (instance null) { // 判定的是是否要加锁synchronized (Singleton.class) {instance new Singleton2();}}return instance;}1.5、懒汉模式 - 内存可见性 指令重排序
当前这个代码中还存在一个重要的问题 如果多个线程都去调用这里的 getlnstance 就会造成大量的读 instance 内存的操作 可能会让编译器把这个读内存操作优化成读寄存器操作 —旦这里触发了优化后续如果第一个线程已经完成了针对 instance 的修改那么紧接着后面的线程都感知不到这个修改仍然把 instance 当成 null
另外还会涉及到指令重排序问题!!
instance new Singleton(); 拆分成三个步骤 1.申请内存空间 2.调用构造方法把这个内存空间初始化成一个合理的对象 3.把内存空间的地址赋值给 instance 引用 正常情况下是按照 123 这个顺序来执行的 编译器还有一手操作指令重排序为了提高程序效率调整代码执行顺序 123 这个顺序就可能变成 132
如果是单线程123 和 132 没有本质区别 例如食堂阿姨打饭1 是拿盘子2 是装饭3 是把盘子给我。此时就是先把盘子给我再装饭
但是多线程环境下就会有问题了!!! 假设 t1 是按照 132 的步骤执行的 t1 执行到 13 之后执行 2 之前被切出 cput2 来执行 (当 t1 执行完 3 之后t2 看起来此处的引用就非空了)此时此刻t2 就相当于直接返回了 instance 引用并且可能会尝试使用引用中的属性 但是由于 t1 中的 2(装饭) 操作还没执行完呢t2 拿到的是非法的对象还没构造完成的不完整的对象
解决方法给 instance 加上 volatile 即可
// 这个代码是完全体的线程安全单例模式
class Singleton2 {// 1就不是立即就初始化实例.private static volatile Singleton2 instance null;// 2把构造方法设为 privateprivate Singleton2() {}// 3提供一个方法来获取到上述单例的实例// 只有当真正需要用到这个实例的时候才会真正去创建这个实例public static Singleton2 getInstance() {if (instance null) { // 判定的是是否要加锁synchronized (Singleton.class) { // 类对象作为锁对象if (instance null) { // 判定的是是否要创建实例instance new Singleton2();}}}return instance;}
}2、案例二阻塞队列
2.1、生产者消费者模型
队列先进先出 阻塞队列同样也是一个符合先进先出规则的特殊队列相比于普通队列阻塞队列又有一些其他方面的功能 1、线程安全 2、产生阻塞效果
1). 如果队列为空执行出队列操作就会出现阻塞阻塞到另一个线程往队列里添加元素队列不为空为止 2). 如果队列为满执行入队列操作也会出现阻塞阻塞到另一个线程从队列里取走元素队列不为满为止 消息队列也是特殊的队列相当于是在阻塞队列的基础上加上了个 消息的类型”按照制定类别进行先进先出 此时咱们谈到的这个消息队列 仍然是一个 “数据结构” 基于上述特性就可以实现 “生产者消费者模型” 此处的阻塞队列就可以作为生产者消费者模型中的交易场所
生产者消费者模型是实际开发中非常有用的一种多线程开发手段尤其是在服务器开发的场景中 假设有两个服务器 ABA作为入口服务器直接接收用户的网络请求B作为应用服务器来给A提供一些数据 优点1解耦合
实现了发送发和接受方之间的解耦
——开发中典型的场景服务器之间的相互调用 客户端发送一个充值请求给 A 服务器此时 A 把请求转发给 B 处理B 处理完了把结果反馈给 A此时就可以视为是 “A 调用了 B”
如果不使用生产者消费者模型 上述场景中A 和 B 之间的耦合性是比较高的! A 要调用 BA 务必要知道 B的存在如果 B 挂了很容易引起 A 的 bug !!!在开发 A 代码的时候就得充分了解到 B 提供的一些接口开发 B 代码的时候也得充分了解到 A 是怎么调用的
另外如果要是再加一个 C 服务器此时也需要对 A 修改不少代码 因此就需要针对 A 重新修改代码重新测试重新发布重新部署非常麻烦了
针对上述场景使用生产者消费者模型就可以有效的降低耦合 对于请求A是生产者B是消费者 对于响应A是消费者B是生产者 阻塞队列都是作为交易场所队列是不变
A 不需要认识 B只需要关注如何和队列交互 (A 的代码中没有任何一行代码和 B 相关) B 不需要认识 A也只需要关注如何和队列交互 (B 的代码中也没有任何一行代码和 A 相关)
如果 B 挂了对于 A 没有任何影响因为队列还好着A 仍然可以给队列插入元素如果队列满就先阻塞就好了 如果 A 挂了也对于 B 没有影响因为队列还好着B 仍然可以从队列取元素如果队列空也就先阻塞就好了 A B 任何一方挂了不会对对方造成影响!!!
新增一个 C 来作为消费者对于 A 来说也完全感知不到… 优点2削峰填谷
能够对于请求进行 “削峰填谷”保证系统的稳定性
——三峡大坝起到的效果就是 “削峰填谷” 到了雨季水流量就会很大三峡大坝关闸蓄水承担了上游的冲击保护下游水流量不是太大不至于出现洪灾——削峰 到了早季水流量很小三峡大坝就开闸放水给下游提供更充分的水源避免出现干旱灾害——填谷 什么时候上游涨水真的是难以预测防患于未然
上游就是用户发送的请求。下游就是一些执行具体业务的服务器。 用户发多少请求不可控的有的时候请求多有的时候请求少… ——未使用生产者消费者模型 未使用生产者消费者模型的时候如果请求量突然暴涨 (不可控)
A暴涨 B暴涨 A 作为入口服务器计算量很轻请求暴涨问题不大 B 作为应用服务器计算量可能很大需要的系统资源也更多如果请求更多了需要的资源进一步增加如果主机的硬件不够可能程序就挂了 ——使用生产者消费者模型 A 请求暴涨 阻塞队列的请求暴涨
由于阻塞队列没啥计算量就只是单纯的存个数据就能抗住更大的压力 B 这边仍然按照原来的速度来消费数据不会因为A的暴涨而引起暴涨B就被保护的很好就不会因为这种请求的波动而引起崩溃
“削峰”这种峰值很多时候不是持续的就一阵过去了就又恢复了 “填谷”B 仍然是按照原有的频率来处理之前积压的数据 实际开发中使用到的 “阻塞队列” 并不是一个简单的数据结构了而是一个 / 一组专门的服务器程序并且它提供的功能也不仅仅是阻塞队列的功能还会在这基础之上提供更多的功能 (对于数据持久化存储支持多个数据通道支持多节点容灾冗余备份支持管理面板方便配置参数.……) 这样的队列又起了个新的名字消息队列” (未来开发中广泛使用到的组件) kafka 就是业界一个比较主流的消息队列消息队列的实现,有很多种核心功能都差不多 2.2、实现阻塞队列 学会使用 Java 标准库中的阻塞队列基于这个内置的阻塞队列实现一个简单的生产者消费者模型 再自己**实现一个简单的阻塞队列 **(为了更好地理解阻塞队列的原理多线程尤其是锁操作) 标准库中的阻塞队列 BlockingQueue
在 Java 标准库中内置了阻塞队列如果我们需要在一些程序中使用阻塞队列直接使用标准库中的即可 BlockingQueue 是一个接口真正实现的类是 LinkedBlockingQueue Queue 提供的方法有三个入队列 offer。出队列 poll。取队首元素 peek 阻塞队列主要方法是两个入队列 put出队列 take BlockingQueue 也有 offer, poll, peek 等方法但是这些方法不带有阻塞特性 import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;public class demo3 {public static void main(String[] args) throws InterruptedException {BlockingQueueString blockingQueue new LinkedBlockingQueue();blockingQueue.put(hello);String s1 blockingQueue.take();System.out.println(s1);blockingQueue.take();String s2 blockingQueue.take();System.out.println(s2);}
}取出 “hello”队列为空此时再次取元素就会进入阻塞等待其他线程往队列中添加元素 生产者消费者模型
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;public class ThreadDemo1 {public static void main(String[] args) {BlockingQueueInteger blockingQueue new LinkedBlockingQueue();// 创建两个线程作为生产者和消费者Thread customer new Thread(() - {while (true) {try {Integer result blockingQueue.take();System.out.println(消费元素 result);} catch (InterruptedException e) {e.printStackTrace();}}});customer.start();Thread producer new Thread(() - {int count 0;while (true) {try {blockingQueue.put(count);System.out.println(生产元素 count);count;Thread.sleep(500); // 每500毫秒生产一个} catch (InterruptedException e) {e.printStackTrace();}}});producer.start();}
}阻塞队列 - 单线程
要实现一个阻塞队列需要先写一个普通的队列再加上线程安全再加上阻塞
队列可以基于数组实现也可以基于链表实现
——链表很容易进行头删 / 尾插 链表的头删操作时间复杂度是 O(1) 链表的尾插操作时间复杂度是 “可以是 O(1) 用一个额外的引用记录当前的尾结点
——数组循环队列 [head, tail) 都指向下标为 0 入队列把新元素放到 tail 位置上并且 tail 出队列把 head 位置的元素返回出去并且 head 当 head / tail 到达数组末尾之后就需要从头开始重新循环
实现循环队列的时候有一个重要的问题如何区分是空队列还是满队列 如果不加额外限制此时队列空或者满都是 head 和 tail 重合 浪费一个格子head tail 认为是空 head tail1 认为是满 额外创建一个变量size 记录元素的个数size 0 空 size arr.length 满
class MyBlockingQueue {// 保存数据的本体private int[] items new int[1000];// 队首下标private int head 0;// 队尾下标private int tail 0;// 有效元素个数private int size 0;// 入队列public void put(int value) {// 1、if (size items.length) {// 队列满了暂时先直接返回return;}// 2、把新的元素放入 tail 位置items[tail] value;tail;// 3、处理 tail 到达数组末尾的情况if (tail items.length) { // 判定 赋值 (虽然是两个操作两个操作都是高效操作)tail 0;}// tail tail % data.length; // 代码可读性差除法速度不如比较不利于开发效率也不利于运行效率// 4、插入完成修改元素个数size;}// 出队列public Integer take() {// 1、if (size 0) {// 如果队列为空返回一个非法值return null;}// 2、取出 head 位置的元素int ret items[head];head;// 3、head 到末尾 重新等于 0if (head items.length) {head 0;}// 4、数组元素个数--size--;return ret;}
}public class TestDemo {public static void main(String[] args) {MyBlockingQueue queue new MyBlockingQueue();queue.put(1);queue.put(2);queue.put(3);queue.put(4);System.out.println(queue.take()); // 1System.out.println(queue.take()); // 2System.out.println(queue.take()); // 3System.out.println(queue.take()); // 4}
}阻塞队列 - 线程安全
当前已经完成了普通队列的实现加上阻塞功能阻塞功能意味着队列要在多线程环境下使用 。保证多线程环境下调用这里的 put 和 take 没有问题的
put 和 take 里面的每一行代码都是在操作公共的变量。既然如此直接就给整个方法加锁即可 (加上 synchronized 已经是线程安全的了)
// 入队列
public void put(int value) {// 此处是把 synchronized 包裹了方法里的所有代码其实 synchronized 加到方法上也是一样的效果synchronized (this) { // 针对同一个 MyBlockingQueue进行 puttake 操作时会产生锁竞争if (size items.length) {return;}items[tail] value;tail;if (tail items.length) {tail 0;}size;}
}// 出队列
public Integer take() {int ret 0;synchronized (this) {if (size 0) {return null;}ret items[head];head;if (head items.length) {head 0;}size--;}return ret;
}阻塞队列 - 阻塞
接下来实现阻塞效果 关键要点使用 wait 和 notify 机制 对于 put 来说阻塞条件就是队列为满对于 take 来说阻塞条件就是队列为空
针对哪个对象加锁就使用哪个对象 wait 如果是针对 this 加锁就 this.wait put 中的 wait 要由 take 来唤醒只要 take 成功了一个元素就队列不满了就可以进行唤醒了 对于 take 中的等待条件是队列为空队列不为空也就是 put 成功之后就来唤醒 当前代码中put 和 take 两种操作不会同时 wait (等待条件是截然不同的一个是为空一个是为满)
如果有人在等待notify 能唤醒如果没人等待notify 没有任何副作用
notify 只能唤醒随机的一个等待的线程不能做到精准 要想精准就必须使用不同的锁对象 想唤醒 t1就 o1.notify让 t1 进行 o1.wait。想唤醒 t2就 o2.notify让 t2 进行 o2.wait
当 wait 被唤醒的时候此时 if 的条件一定就不成立了嘛?? 具体来说put 中的 wait 被唤醒要求队列不满 但是 wait 被唤醒了之后队列一定是不满的嘛 注意咱们当前代码中确实不会出现这种情况当前代码一定是取元素成功才唤醒每次取元素都会唤醒 但是稳妥起见最好的办法是 wait 返回之后再次判定一下看此时的条件是不是具备了!!
将 if 改为 while标准库就是建议这么写的
while (size items.length) {// 队列满了暂时先直接返回// return;this.wait();
} while (size 0) {// 如果队列为空返回一个非法值// return null;this.wait();
}代码 import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;// 自己写的阻塞队列此处不考虑泛型直接使用 int 来表示元素类型了
class MyBlockingQueue {// 保存数据的本体private int[] items new int[1000];// 队首下标private int head 0;// 队尾下标private int tail 0;// 有效元素个数private int size 0;// 入队列public void put(int value) throws InterruptedException {synchronized (this) { // 针对同一个 MyBlockingQueue进行 puttake 操作时会产生锁竞争while (size items.length) {// 队列满了暂时先直接返回// return;this.wait();}// 2、把新的元素放入 tail 位置items[tail] value;tail;// 3、处理 tail 到达数组末尾的情况if (tail items.length) { // 判定 赋值 (虽然是两个操作两个操作都是高效操作)tail 0;}// tail tail % data.length; // 代码可读性差除法速度不如比较不利于开发效率也不利于运行效率// 4、插入完成修改元素个数size;// 如果入队列成功则队列非空唤醒 take 中的 waitthis.notify();}}// 出队列public Integer take() throws InterruptedException {int ret 0;synchronized (this) {while (size 0) {// 如果队列为空返回一个非法值// return null;this.wait();}// 2、取出 head 位置的元素ret items[head];head;// 3、head 到末尾 重新等于 0if (head items.length) {head 0;}// 4、数组元素个数--size--;// take 成后唤醒 put 中的 waitthis.notify();}return ret;}
}public class ThreadDemo2 {public static void main(String[] args) {// 生产者消费者模型BlockingQueueInteger blockingQueue new LinkedBlockingQueue();// 创建两个线程作为生产者和消费者Thread customer new Thread(() - {while (true) {try {Integer result blockingQueue.take();System.out.println(消费元素 result);} catch (InterruptedException e) {e.printStackTrace();}}});customer.start();Thread producer new Thread(() - {int count 0;while (true) {try {blockingQueue.put(count);System.out.println(生产元素 count);count;Thread.sleep(500); // 每500毫秒生产一个} catch (InterruptedException e) {e.printStackTrace();}}});producer.start();}
}3、案例三定时器
3.1、标准库中的定时器 Timer
定时器也是软件开发中的一个重要组件类似于一个 “闹钟”达到一个设定的时间之后就唤醒并执行之前设定好的任务
生活中闹钟有两种风格1.指定特定时刻提醒。2.指定特定时间段之后提醒 这里的定时器不是提醒是执行一个实现准备好的方法/代码 定时器是一种实际开发中非常常用的组件 比如网络通信中很容易出现 “连不上” 的情况不能一直等就可以使用定时器来进行 “止损”如果对方 500ms 内没有返回数据则断开连接尝试重连 比如一个 Map希望里面的某个 key 在 3s 之后过期(自动删除) 类似于这样的场景就需要用到定时器 join (指定超时时间)sleep (休眠指定时间是基于系统内部的定时器来实现的)
先介绍标准库的定时器用法然后再看看如何自己实现一个定时器
标准库中提供了一个 Timer 类Timer 类的核心方法为 schedule (安排)这个方法的效果是给定时器注册一个任务任务不会立即执行而是在指定时间进行执行 schedule 包含两个参数第一个参数指定即将要执行的任务代码 (Runnable)第二个参数指定多长时间之后执行 (单位为毫秒)
import java.util.Timer;
import java.util.TimerTask;public class demo5 {public static void main(String[] args) {Timer timer new Timer();timer.schedule(new TimerTask() {Overridepublic void run() {System.out.println(hello time);}}, 3000);timer.schedule(new TimerTask() {Overridepublic void run() {System.out.println(hello time2);}}, 2000);System.out.println(main);}
}运行结果 首先打印main 几秒后 打印hello time2 然后打印hello time 但是程序没有结束
Timer 内部是有专门的线程来负责执行注册的任务的 Timer 内部都需要 管理很多的任务执行时间到了的任务 自己实现一个定时器一个定时器是可以注册 N 个任务的N 个任务会按照最初约定的时间按顺序执行
1). 有一个扫描线程负责判定时间到/执行任务 (单独在定时器内部搞个线程让这个线程周期性地扫描判定任务是否是到时间了如果到时间了就执行没到时间就再等等)
2). 还要有一个数据结构(优先级队列)来保存所有被注册的任务 3.2、描述任务
创建一个专门的类来表示一个定时器中的任务 (TimerTask)
队列中存放的任务就是 RunnableRunnable 只是描述了任务内容还需要描述任务什么时候被执行
// 创建一个类表示一个任务
class MyTask {// 任务具体要做什么private Runnable runnable;// 任务什么时候执行 (任务要执行的毫秒级时间戳)private long time;public MyTask(Runnable runnable, long time) {this.runnable runnable;this.time time;}// 获取当前任务时间public long getTime() {return time;}// 执行任务public void run() {runnable.run();}
}3.2、组织任务
使用一定的数据结构把一些任务给放到一起通过一定的数据结构来组织
假设现在有多个任务过来了—个小时之后去做作业三个小时之后去上课10分钟之后去休息—会 安排任务的时候这些任务的顺序是无序的但是执行任务的时候这就不是无序的了按照时间先后来执行 咱们的需求就是能够快速找到所有任务中时间最小的任务
此时我们发现可以用堆在标准库中有一个专门的数据结构 PriorityQueue
咱们这里的每个任务都是带个时间多久之后执行一定是时间越靠前就先执行 按照时间小的作为优先级高 此时队首元素就是整个队列中最先要执行的任务 虽然队列中的元素顺序不能完全确定但是可以知道队首元素一定是时间最靠前的 此时扫描线程只需要扫一下队首元素即可不必遍历整个队列
private PriorityQueue queue new PriorityQueue();
但是此处的优先级队列要在多线程环境下使用要考虑到线程安全问题可能在多个线程里进行注册任务同时还有一个专门的线程来取任务执行此处的队列就需要注意线程安全问题 所以我们得使用 PriorityBlockingQueue 既带有优先级又带有阻塞队列
private PriorityBlockingQueue queue new PriorityBlockingQueue();
// 自己写个简单的定时器
class MyTimer {// 扫描线程private Thread t null;// 定时器内部要能够存放多个任务 阻塞优先级队列保存任务private PriorityBlockingQueueMyTask queue new PriorityBlockingQueue();public MyTimer() {// TODO}/** 定时器提供一个 schedule 方法注册任务* param runnable 要执行的任务* param after 多长时间(毫秒)之后执行*/public void schedule(Runnable runnable, long after) {MyTask task new MyTask(runnable, System.currentTimeMillis() after);queue.put(task); // 任务放入堆}
}——执行时间到了的任务 需要先执行时间最考前的任务 就需要有一个扫描线程不停地去检查当前优先队列的队首元素看看当前最靠前的这个任务是不是时间到了
在定时器构造方法中 创建线程进行扫描
阻塞队列只能先把元素出队列才好判定不满足还得放回去 这不像普通队列可以直接取队首元素判定的
public MyTimer() {t new Thread(() - {while (true) {try {// 取出队首元素再比较这个任务有没有到时间MyTask myTask queue.take();long curTime System.currentTimeMillis();if (curTime (myTask).getTime()) { // 1.没到时间任务放回堆queue.put(myTask);} else { // 2.时间到了执行任务myTask.run();}} catch (InterruptedException e) {e.printStackTrace();}}});t.start();
}3.3、两个缺陷
上述代码中存在两个非常严重的问题
第—个缺陷 MyTask 没有指定比较规则 像刚才咱们实现的 MyTask 这个类的比较规则并不是默认就存在的这个需要咱们手动指定按照时间大小来比较的 标准库中的集合类很多都是有一定的约束限制的不是随便拿个类都能放到这些集合类里面去的
——测试
public class ThreadDemo {public static void main(String[] args) {MyTimer myTimer new MyTimer();myTimer.schedule(new Runnable() {Overridepublic void run() {System.out.println(任务1);}}, 1000);myTimer.schedule(new Runnable() {Overridepublic void run() {System.out.println(任务2);}}, 2000);}
}让 MyTask 类实现 Comparable接口另外也可以使用 Comparator单独写个比较器 修改 class MyTask implements ComparableMyTask { Overridepublic int compareTo(MyTask o) {return (int) (this.time - o.time);}
}第二个缺陷 如果不加任何限制这个循环就会执行的非常快
while (true) 转的太快了, 造成了无意义的 CPU 浪费
如果队列中的任务是空着的就还好这个线程就再这里阻塞了 (没问题) 就怕队列中的任务不空并且任务时间还没到 上述操作称为 “忙等”等确实是等了但是又没闲着。既没有实质性的工作产出同时又没有进行休息 等待是要释放 CPU 资源的。让 CPU 干别的事情。但是忙等。既进行了等待。又占用着CPU资源忙等这种操作是非常浪费 CPU 的。
既然是指定一个等待时间为啥不直接用 sleep 而是要再用一下 wait 呢 sleep 不能被中途唤醒的wait 能够被中途唤醒 在等待过程中可能要插入新的任务 新的任务是可能出现在之前所有任务的最前面的使用 sleep 可能会错过新任务的执行时间
可以基于 wait 这样的机制来实现 wait 有一个版本指定等待时间 不需要 notify时间到了自然唤醒计算出当前时间和任务的目标之间的时间差就等待这么长时间即可
在 schedule 操作中就需要加上一个 notify 操作。使用 wait 等待每次有新任务来了 (有人调用 schedule)就 notify 一下重新检查下时间重新计算要等待的时间
这样扫描线程既可以指定时间等待也可以随时唤醒。让等待不占用 CPU同时不错过新任务 修改 3.4、问题三notify 空
代码写到这里还有个很严重的问题这个问题还是和线程安全 / 随机调度密切相关的 考虑一个极端情况 假设代码执行到 put 这一行这个线程就从 cpu 调度走了… 当线程回来之后接下来就要进行 wait 操作此时 wait 的时间已经是算好了的 比如 curTime 是 13:00任务 getTime 是 14:00 即将要 wait 1小时 (此时还没执行 wait因为线程在 put 就被调走了)
此时另一个线程调用了 schedule 添加新任务新任务是 13:30 执行 此处调用 schedule 会执行 notify通知 wait 唤醒
由于扫描线程 wait 还没执行呢 所以此处的 notify 不会产生任何的唤醒操作 此时此刻新的任务虽然已经插入了队列新的任务也是在队首紧接着扫描线程回到 cpu了此时等待时间仍然是 1小时 因此13:30 新的任务就被错过了
了解了上述问题之后就不难发现问题出现的原因是因为当前 take 操作和 wait 操作并非是原子的 如果在 take 和 wait 之间加上锁保证在这个过程中不会有新的任务过来问题自然解决 (换句话说只要保证每次 notify 时确实都正在 wait) 修改 此处只需要把锁的范围放大放大之后此时就可以保证执行 notify 的时候wait 是确实已经执行完了 就可以预防出现 notify 的时候还没有准备好wait这样的情况了 代码 import java.util.concurrent.PriorityBlockingQueue;// 创建一个类表示一个任务
class MyTask implements ComparableMyTask {// 任务具体要做什么private Runnable runnable;// 任务什么时候执行 (任务要执行的毫秒级时间戳)private long time;public MyTask(Runnable runnable, long time) {this.runnable runnable;this.time time;}// 获取当前任务时间public long getTime() {return time;}// 执行任务public void run() {runnable.run();}Overridepublic int compareTo(MyTask o) {return (int) (this.time - o.time);}
}// 自己写个简单的定时器
class MyTimer {// 扫描线程private Thread t null;// 定时器内部要能够存放多个任务 阻塞优先级队列保存任务private PriorityBlockingQueueMyTask queue new PriorityBlockingQueue();// 扫描线程public MyTimer() {t new Thread(() - {while (true) {try {synchronized (this) {// 取出队首元素再比较这个任务有没有到时间MyTask myTask queue.take();long curTime System.currentTimeMillis();if (curTime (myTask).getTime()) { // 1.没到时间任务放回堆queue.put(myTask);// 在 put 后 waitthis.wait(myTask.getTime() - curTime);} else { // 2.时间到了执行任务myTask.run();}}} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}/** 定时器提供一个 schedule 方法注册任务* param runnable 要执行的任务* param after 多长时间(毫秒)之后执行*/public void schedule(Runnable runnable, long after) {// 注意换算time 是一个时间戳不是绝对的时间戳的值MyTask task new MyTask(runnable, System.currentTimeMillis() after);queue.put(task); // 任务放入堆// 有新任务加入 notifysynchronized (this) {this.notify();}}
}public class ThreadDemo {public static void main(String[] args) {MyTimer myTimer new MyTimer();myTimer.schedule(new Runnable() {Overridepublic void run() {System.out.println(任务1);}}, 1000);myTimer.schedule(new Runnable() {Overridepublic void run() {System.out.println(任务2);}}, 2000);}
}运行结果
任务1
任务2总结 描述—个任务 runnable time使用优先阻塞队列来组织若干个任务PriorityBlockingQueue实现 schedule 方法来注册任务到队列中创建一个扫描线程这个扫描线程不停地获取到队首元素并且判定时间是否到达注意让 MyTask 类能够支持比较注意解决这里的忙等问题notity 时 wait 没有执行问题 4、案例四线程池
4.1、用户态 / 内核态
进程比较重频繁创建销毁开销大 解决方案进程池 or 线程
线程 (轻量级进程)虽然比进程轻了创建线程比创建进程更高效销毁线程比销毁进程更高效调度线程比调度进程更高效…但是如果创建销毁的频率进一步增加仍然会发现开销还是有的 解决方案线程池 or 协程/纤程 (还没有被加入 Java 标准库。Go 内置了协程因此使用 Go 开发并发编程程序是有一定优势的)
使用线程池来降低创建/销毁线程的开销
把线程提前创建好放到池子里 1.后面需要用线程直接从池子里取就不必从系统这边申请了。线程用完了也不是还给系统而是2.放回池子里以备下次再用 这两个动作比创建/销毁更高效的
——为森么线程放在池子里就比从系统这边申请释放来的更快呢 程序中的“用户态” 用户态执行的是程序猿自己写的代码就在最上面的应用程序这一层来运行的。这里的代码都称为 “用户态” 运行的代码。
程序中的内核态 内核会给程序提供一些 API称为系统调用有些代码需要调用操作系统的 API进一步的逻辑就会在内核中执行内核态进行的操作都是在操作系统内核中完成的。
例如调用一个 System.out.println。本质上要经过 write 系统调用进入到内核中内核执行—堆逻辑控制显示器输出字符串…
在内核中运行的代码称为 “内核态” 运行的代码。
创建/销毁线程需要操作系统内核完成 (创建线程本质是在内核中搞个PCB加到链表里) 调用的 Thread.start 其实归根结底也是要进入内核态来运行。
此时你不清楚内核身上背负着多少任务 (内核不是只给你一个应用程序服务给所有的程序都要提供服务) 因此当使用系统调用执行内核代码的时候无法确定内核都要做哪些工作整体过程 不可控” 的
而把创建好的线程放到 池子里由于池子就是用户态实现的 这个放到池子 / 从池子取这个过程不需要涉及到内核态就是纯粹的用户态代码就能完成
一般认为纯用户态的操作效率要比经过内核态处理的操作要效率更高。
例如滑稽老铁去银行处理业务柜员说需要省份证复印件 1、滑稽老铁自己来到大厅的复印机这里进行复印。纯用户态的操作。(完全自己完成的整体的过程可控) 2、滑稽老铁把身份证给柜员让柜员去帮他复印这个过程就相当于交给了内核态完成一些工作。(不是自己完成的整体不可控的) 咱们也不知道柜员身上有多少任务。可能从柜台消失之后是给你复印去了。 但是他可能还会顺手做一些其他的事情。数一下钱 / 清点一下票据 / 上个厕所 / 回个消息…
认为内核态效率低倒不是说—定就真的低。而是代码进入了内核态就不可控了。 内核什么时候给你把活干完把结果给你。(有的时候快有的时候慢) 4.2、标准库中的线程池 ThreadPoolExecutor
ThreadPoolExecutor 先学习—下 Java 标准库中线程池的使用然后再自己实现一个线程池 标准库的线程池叫做 ThreadPoolExecutor 这个东西用起来有点麻烦 在 java.util.concurrent (concurrent 并发) 下 Java 中很多和多线程相关的组件都在这个 concurrent 包里 ——构造方法
(针对 ThreadPoolExecutor 这里的构造方法参数的解释是高频考点重点掌握!!!)
ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)Creates a new ThreadPoolExecutor with the given initial parameters.// 创建一个新 ThreadPoolExecutor 给定的初始参数int corePoolSize 核心线程数 (正式员工的数量)
int maximumPoolSize 最大线程数 (正式员工 临时工) 把一个线程池想象成是一个公司公司里有很多员工在干活 把线程(员工)分成两类 1、正式员工(核心线程)正式员工允许摸鱼 2、临时工临时工不允许摸鱼 开始的时候假设公司要完成的工作不多正式员工完全就能搞定就不需要临时工。 如果公司的任务突然猛增了正式员工加班也搞不定了就需要雇佣一批临时工 (更多的线程) 但是一个程序任务不一定始终都很多过了一段时间工作量又降低了现在的活正式员工也就能搞定了甚至还有富裕 (正式员工可以摸鱼了) 临时工就更摸鱼了就需要对现有的线程(临时工)进行一定的淘汰 整体的策略正式员工保底临时工动态调节
long keepAliveTime 允许临时工摸鱼的时间
TimeUnit unit 时间的单位 (s, ms, us…)
BlockingQueueRunnable workQueue, 任务队列 线程池会提供一个 submit 方法让程序猿把任务注册到线程池中加到这个任务队列中 每个工作线程都是再不停尝试 take 的如果有任务take 成功没有就阻塞。
ThreadFactory threadFactory , 线程工厂类用于创建线程线程池是需要创建线程的
RejectedExecutionHandler handler 描述了线程池的 拒绝策略也是一个特殊的对象描述了当线程池任务队列满了如果继续添加任务会有什么样的行为…
以下是标准库提供的四个拒绝策略
直接抛异常 RejectedExecutionException多出来的任务谁加的谁负责执行直接丢弃最老的任务丢弃最新的任务
比如我现在有很多任务要完成突然有人给我来了个新的活但是我已经非常忙任务队列已经满了导致我 CPU 烧了新的活干不了 (1) 我说我没空你自己干吧 (2) 放下手里的工作去做新的活 (3) 拒绝新的活还是做原有的工作 (4) 线程池中线程的个数
虽然线程池的参数这么多但是使用的时候最最重要的参数还是第一组参数线程池中线程的个数 ——有一个程序这个程序要 并发的/多线程的 来完成一些任务如果使用线程池的话这里的线程数设为多少合适 [不仅仅是面试题,也是工作中需要思考的话题] 针对这个问题网上的很多说法是不正确的 网上一种典型的回答假设机器有 N 核CPU线程池的线程数目就设为 N(CPU 的核数)N 11.2N1.5N 2N… 只要能回答出一个具体的数字都—定是错的
不同的程序特点不同此时要设置的线程数也是不同的 考虑两个极端情况 CPU 密集型 每个线程要执行的任务都是狂转 CPU (进行一系列算术运算) 此时线程池线程数最多也不应该超过 CPU 核数 此时如果你设置的更大也没用 CPU 密集型任务要一直占用 CPU搞那么多线程但是 CPU 的坑不够了… IO 密集型 每个线程干的工作就是等待 IO (读写硬盘读写网卡等待用户输入) ——不吃CPU 此时这样的线程处于阻塞状态不参与 CPU 调度… 这个时候多搞一些线程都无所谓 不再受制于 CPU 核数了 理论上来说你线程数设置成无穷大都可以 (实际上当然是不行的)
然而我们实际开发中并没有程序符合这两种理想模型… 真实的程序往往一部分要吃 CPU一部分要等待 IO 具体这个程序几成工作量是吃 CPU 的几成工作量是等待 IO不确定…
实践中确定线程数量通过性能测试的方式找到合适的值
例如写一个服务器程序服务器里通过线程池多线程的处理用户请求就可以对这个服务器进行性能测试
比如构造一些请求发送给服务器要测试性能这里的请求就需要构造很多比如每秒发送 500 / 1000 / 2000…根据实际的业务场景构造一个合适的值
根据这里不同的线程池的线程数来观察程序处理任务的速度程序持有的 CPU 的占用率 当线程数多了整体的速度是会变快但是 CPU 占用率也会高 当线程数少了整体的速度是会变慢但是 CPU 占用率也会下降 需要找到一个让程序速度能接受并且CPU占用也合理这样的平衡点
不同类型的程序因为单个任务里面 CPU 上计算的时间和阻塞的时间是分布不相同的 因此随意想出来一个数字往往是不靠谱
搞了多线程就是为了让程序跑的更快嘛为啥要考虑不让CPU占用率太高呢 对于线上服务器来说要留有一定的冗余随时应对一些可能的突发情况(例如请求突然暴涨) 如果本身已经把 CPU 快占完了这时候突然来—波请求的峰值此时服务器可能直接就挂了 Executors
ThreadPoolExecutor 这个线程池用起来更麻烦一点(提供的功能更强大)所以才提供了工厂类让我们用着更简单
标准库中提供了一个简化版本的线程池 Executors 本质是针对 ThreadPoolExecutor 进行了封装提供了一些默认参数
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class demo6 {public static void main(String[] args) {// 创建一个固定的线程数目的线程池参数指定了线程的个数ExecutorService pool Executors.newFixedThreadPool(10);// 创建一个自动扩扩容的线程池线程数量动态变化会根据任务量自动扩容Executors.newCachedThreadPool();// 创建一个只有一个线程的线程池Executors.newSingleThreadExecutor();// 创建一个带有定时器功能的线程池类似于 Timer只不过执行的时候不是由扫描线程自己执行而是由单独的线程池来执行Executors.newScheduledThreadPool(10);}
}——使用 Executors 构造出一个 10 个线程的线程池 线程池提供了一个重要的方法 submit 可以给线程池提交若干个任务
把 Runnable 描述的任务提交到线程池里此时 run 方法不是主线程调用是由线程池中的 10 个线程中的一个调用
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class demo6 {public static void main(String[] args) {ExecutorService pool Executors.newFixedThreadPool(10);pool.submit(new Runnable() {Overridepublic void run() {System.out.println(hello threadPool!);}});}
}运行结果
hello threadPool!运行程序之后发现main 线程结束了但是整个进程没结束线程池中的线程都是前台线程此时会阻止进程结束 (前面定时器 Timer 也是同理) ——循环提交 1000 个任务 public class ThreadDemo2 {public static void main(String[] args) {ExecutorService pool Executors.newFixedThreadPool(10);for (int i 0; i 1000; i) {int n i;pool.submit(new Runnable() {Overridepublic void run() {System.out.println(hello pool! n);}});}}
}此处要注意当前是往线程池里放了 1000 个任务 1000 个任务就是由这 10 个线程来平均分配一下差不多是一人执行 100 个但是注意这里并非是严格的平均可能有的多一个有的少一个都正常 (随机调度) (每个线程都执行完一个任务之后再立即取下一个任务… 由于每个任务执行时间都差不多因此每个线程做的任务数量就差不多)
进一步的可以认为这 1000 个任务就在一个队列中排队呢 这 10 个线程就依次来取队列中的任务取一个就执行一个执行完了之后再执行下一个 工厂模式
ExecutorService pool Executors.newFixedThreadPool(10); 此处 new 是方法名字的一部分不是 new 关键字 这个操作使用某个类的某个静态方法直接构造出一个对象来 (相当于是把 new 操作给隐藏到这样的方法后面了)
像这样的方法就称为“工厂方法” 提供这个工厂方法的类也就称为工厂类此处这个代码就使用了“工厂模式这种设计模式
工厂模式—句话表示使用普通的方法来代替构造方法创建对象 为啥要代替构造方法有坑!!! 坑就体现在只构造一种对象好办 如果要构造多种不同情况的对象就难搞了… ——举个栗子 有个类用多种方法构造平面上的一个点 class Point {// 使用笛卡尔坐标系提供的坐标,来构造点public Point(double x, double y) {}// 使用极坐标,来构造点public Point(double r, double a) {}
}很明显这个代码有问题!!! 正常来说多个构造方法 是通过重载”的方式来提供的 重载要求的是方法名相同参数的个数或者类型不相同
而上述两个方法方法名相同参数个数相同参数类型相同无法构成重载在 Java 上无法正确编译
为了解决这个问题就可以使用工厂模式
class PointFactory {public static Point makePointByXY(double x, double y) {}public static Point makePointByRA(double r, double a) {}
}Point p PointFactory.makePointByXY(10,20);普通方法方法名字没有限制的 因此有多种方式构造就可以直接使用不同的方法名即可此时方法的参数是否要区分已经不重要了
很多时候设计模式是在规避编程语言语法上的坑 不同的语言语法规则不一样因此在不同的语言上能够使用的设计模式可能会不同有的设计模式已经被融合在语言的语法内部了… 咱们日常谈到的设计模式主要是基于 C/Java/C# 这样语言来展开的这里所说的设计模式不一定适合其他语言
像工厂模式对于 Python 来说没什么价值Python 构造方法不像C/Java 的这么坑可以直接在构造方法中通过其他手段来做出不同版本的区分 ——不能直接使用 i 的原因 Lambda 变量捕获
很明显此处的 run 方法属于 Runnable这个方法的执行时机不是立刻马上 而是在未来的某个节点 (后续在线程池的队列中排到他了就让对应的线程去执行)
fori 循环中的 i这是主线程里的局部变量 (在主线程的栈上)随着主线程这里的代码块执行结束就销毁了 很可能主线程这里 for 执行完了当前 run 的任务在线程池里还没排到呢此时 i 就已经要销毁了 为了避免作用域的差异导致后续执行 run 的时候 i 已经销毁 于是就有了变量捕获也就是让 run 方法把刚才主线程的 i 给往当前 run 的栈上拷贝一份… (在定义 run 的时候偷偷把 i 当前的值记住 后续执行 run 的时候就创建一个也叫做 i 的局部变量并且把这个值赋值过去…)
在 Java 中对于变量捕获做了一些额外的要求 在 JDK 1.8 之前要求变量捕获只能捕获 final 修饰的变量后来发现这么搞太麻烦了 在 1.8 开始放松了一点标准要求不一定非得带 final 关键字只要代码中没有修改这个变量也可以捕获
此处i 是有修改的不能捕获的 而n是没有修改的虽然没有 final 修饰但是也能捕获了
C, JS 也有类似的变量捕获的语法但是没有上述限制… 4.3、实现一个线程池 线程池里面有 先能够描述任务 (直接使用 Runnable)需要组织任务 (直接使用 BlockingQueue)能够描述工作线程还需要组织这些线程需要实现往线程池里添加任务 import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;// 实现一个固定线程数的线程池
class MyThreadPool {// 1、描述一个任务不像定时器涉及时间直接用 Runnable不需要额外类// 2、使用一个数据结构(阻塞队列)来组织若干个任务private BlockingQueueRunnable queue new LinkedBlockingQueue();// 在构造方法中创建若干个线程 (n 表示线程的数量)public MyThreadPool(int n) {// 在这里创建线程for (int i 0; i n; i) {Thread t new Thread(() - {while (true) { // 从队列中循环地取任务try {// 循环地获取任务队列中的任务然后执行// 队列为空直接阻塞。队列非空就获取内容Runnable runnable queue.take(); // 获取任务runnable.run(); // 执行任务} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}}// 创建一个方法能够允许程序员放任务到线程池中// 注册任务给线程池由这 10 个线程执行public void submit(Runnable runnable) {try {queue.put(runnable);} catch (InterruptedException e) {e.printStackTrace();}}
}public class TestDemo {public static void main(String[] args) {MyThreadPool pool new MyThreadPool(10);for (int i 0; i 1000; i) {int n i;pool.submit(new Runnable() {Overridepublic void run() {System.out.println(hello n);}});}}
}