✏️作者:银河罐头
📋系列专栏:JavaEE
🌲“种一棵树最好的时间是十年前,其次是现在”
目录
- 多线程案例
- 阻塞队列
- 阻塞队列是什么
- 生产者消费者模型
- 标准库中的阻塞队列
- 阻塞队列实现
- 定时器
- 定时器是什么
- 标准库中的定时器
- 实现定时器
- 线程池
- 线程池是什么
- 标准库中的线程池
- 实现线程池
多线程案例
阻塞队列
阻塞队列是什么
阻塞队列,也是一个队列,之前数据结构中学过队列的特点是先进先出。
实际上还有一些特殊的队列,不一定非得遵守先进先出的规则。
比如优先级队列 PriorityQueue.
阻塞队列,也是先进先出的,但是带有特殊的功能:阻塞。
如果队列为空,执行出队列操作,就会阻塞,阻塞到另一个线程往队列里添加元素(队列不空)为止
如果队列满了,执行入队列操作,就会阻塞,阻塞到另一个线程从队列里取走元素(队列不满)为止
消息队列,也是特殊的队列,相当于是在阻塞队列的基础上加上了个"消息的类型",按照指定类别进行先进先出。
举个栗子🌰:医院里有个科室人很多,超声科,B超。B超能检查胃,肾,心脏,胎儿。
此处谈到的"消息队列",仍然是一个"数据结构",
因为这个消息队列太香了,因此就有大佬把这样的数据结构,单独实现成了一个程序。这个程序可以通过网络的方式和其他程序进行通信。
此时,这个消息队列,就可以单独部署到一组服务器上(分布式),存储能力和转发能力都大大提高了。很多大型项目里,就可以看到这样的消息队列的身影。
此时,消息队列,就已经成了一个可以和mysql , redis相提并论的一个重要组件了。“中间件”
要想认识清楚消息队列,还是得认识清楚"阻塞队列"。
为啥消息队列这么香?和阻塞队列阻塞特性关系很大。
基于这样的特性,可以实现"生产者消费者模型"
生产者消费者模型
举个栗子🌰:过年有个环节就是年夜饭家里人一起包饺子。
包饺子的步骤:擀饺子皮 + 包饺子
两种典型的包法:
1.每个人都进行擀饺子皮 + 包饺子 的操作。
(这种情况大家会竞争唯一的擀面杖,就会产生阻塞等待影响效率)
2.一个人专门负责擀饺子皮,剩下其他人负责包饺子。每擀好一个饺子皮,就放到盖帘上,其他人从盖帘上取一个皮来包饺子。
很明显,第二种包法更好,这种方式就称为 生产者消费者 模型,擀饺子皮的人是 生产者,剩下其他包饺子的人是 消费者,盖帘就是阻塞队列。如果生产者擀饺子皮擀太慢了(盖帘空了),包饺子的人就得等;如果擀太快了,盖帘满了,生产者就得等(包饺子的人把饺子皮取走)。
生产者消费者 模型,能给我们的程序带来两个非常重要的好处。
- 1.实现了发送方和接收方之间的"解耦"
开发中典型的场景:服务器之间的相互调用
此时A 把请求转发给 B 处理,B 处理完了把结果反馈给 A,此时就可以视为 “A 调用了 B”。上述场景中,A 和 B 之间的耦合是比较高的。A 调用 B ,A 务必要知道 B 的存在,如果 B 挂了就很容易引起 A 的 bug !
另外,如果要是再加一个 C 服务器,此时就需要对 A 修改不少代码,因此就需要针对 A 重新修改代码,重新测试,重新发布,重新部署…这样就很麻烦
针对上述场景,使用 生产者消费者 模型就可以有效降低耦合
此时 A 和 B 之间的耦合就降低很多了。
A 是不知道 B 的,A 只知道队列( A 的代码中没有关于 B 的);
B 也是不知道 A 的,B 只知道队列( B 的代码中没有关于 A 的)。
如果 B 挂了对 A 没有任何影响,因为队列还是正常的,A 仍然可以给队列里插入元素,如果队列满了就先阻塞;如果 A 挂了对 B 没有任何影响,因为队列还是正常的,B 仍然可以从队列里取出元素,如果队列空了就先阻塞。
总之 A , B 任何一方挂了对 对方都不会造成影响
新增一个 C 作为消费者,对于 A 来说仍然是无感知的
- 2.“削峰填谷”,保证系统的稳定性。
举个栗子🌰:三峡大坝,起到的效果就是 “削峰填谷”。
如果上游水多了,三峡大坝关闸蓄水,此时就相当于三峡大坝承担了上游的冲击,保护了下游(削峰);
如果上游水少了,三峡大坝开闸放水,有效保证下游的用水情况,避免出现干旱灾害(填谷)。
服务器开发,也和上述模型非常类似。
咱们的上游就是用户发送的请求,下游就是一些执行具体业务的服务器。
用户发多少请求时不可控的,有时候请求多,有时候请求少。
举个栗子🌰:“热搜”
突然某个瞬间,很多用户都发送请求,此时如果没有充分的准备(使用生产者消费者模型是一个有效的手段),服务器一下扛不住就挂了。
标准库中的阻塞队列
Queue 提供的方法有:1.入队列 offer 2.出队列 poll 3.取队首元素 peek
阻塞队列的主要方法是 2 个:1.入队列 put 2. 出队列 take (这 2 个方法都是带有阻塞功能的)
package Thread;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
//阻塞队列的使用
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
blockingQueue.put("hello");
String res = blockingQueue.take();
System.out.println(res);
}
}
//输出:
hello
package Thread;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
//阻塞队列的使用
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
blockingQueue.put("hello");
String res = blockingQueue.take();
System.out.println(res);
res = blockingQueue.take();//如果队列空了再取元素就会阻塞
System.out.println(res);
}
}
//输出:
hello
(程序没有结束)
package Thread;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ThreadDemo{
public static void main(String[] args) {
BlockingQueue<Integer> 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);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
}
}
阻塞队列实现
要想实现一个阻塞队列,首先要实现一个普通的队列。
这个普通的队列:1.基于链表实现(头删尾插) 2.基于数组实现 , 环形队列:[head,rear)
普通队列加上阻塞功能。
阻塞功能意味着队列要在多线程环境下使用。
要保证线程安全。所以要加上锁,使用 synchronized。
//基于数组循环队列自己写的阻塞队列
package Thread;
class MyBlockingQueue{
private int[] items = new int[1000];
private int head = 0;
private int tail = 0;
private int size = 0;
//入队列
public void put(int val) throws InterruptedException {
//判满
synchronized (this) {
while(size == items.length){//可能 wait()唤醒之后队列还是满的
//return;
this.wait();//队列满了,此时要发生阻塞
}
items[tail] = val;
tail++;
//1)
// tail = tail % items.length;//进行除法操作,更慢的操作
//2)
if(tail >= items.length){
tail = 0;
}
//2)比 1)可读性更好,代码效率比 % 高。判断 + 赋值,虽然是 2 个操作,2 个操作都是高效操作
size++;
this.notify();//唤醒 take里的 wait
}
}
//出队列
public Integer take() throws InterruptedException {
//判空
int result = 0;
synchronized (this) {
while(size == 0){
//return null;
this.wait();//队列空了,此时要发生阻塞
}
result = items[head];
head++;
if(head >= items.length){
head = 0;
}
size--;
this.notify();//唤醒 put 里的 wait
}
return result;
}
}
public class ThreadDemo{
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue queue = new MyBlockingQueue();
Thread customer = new Thread(()->{
while(true){
try {
int result = queue.take();
System.out.println("消费元素: " + result);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
Thread producer = new Thread(()->{
int count = 0;
while(true){
try {
System.out.println("生产元素: " + count);
queue.put(count);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
}
}
while(size == 0){
//return null;
this.wait();//队列空了,此时要发生阻塞
}标准库建议是这么写的
定时器
定时器是什么
闹钟:1.指定特定时间提醒 2.指定特定时间段之后提醒(定时器)
这里的定时器,不是提醒,而是执行一个准备好的方法/代码
这个是开发中常用的一个组件,尤其是网络编程的时候。网络有时候可能不太顺畅,很容易出现"卡了"“连不上"的情况,就可以使用定时器来"及时止损”。
标准库中的定时器
package Thread;
import java.util.Timer;
import java.util.TimerTask;
public class ThreadDemo{
public static void main(String[] args) {
System.out.println("程序启动");
Timer timer = new Timer();//这个Timer类就是标准库的定时器
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("运行定时器任务");
}
},3000);
}
}
timer.schedule() 这个方法的效果是给定时器注册一个任务,任务不会立即执行,而是在指定时间执行。
实现定时器
自己来实现一个定时器:
1.让被注册的任务,在指定时间去执行单独在定时器内部创建一个线程,让这个线程周期性的扫描,判断任务是否到时间了,到时间了就执行,没到时间的就继续等。
2.一个定时器可以执行 N 个任务。N个任务会按照指定的时间,按顺序执行。
这 N 个任务需要用数据结构来保存。用优先级队列这个数据结构。
每个任务都带有自己的时间,多久执行,时间越靠前的越先执行。
时间小的优先级高,队首元素就是最先执行的任务,扫描线程只需要扫一下队首元素就可以(不必遍历整个队列)
总结:1.有一个扫描线程,判断到时间执行任务 2. 用优先级队列来保存所有被注册的任务
此处的优先级队列会在多线程环境下使用。
调用schedule 是一个线程,扫描是另一个线程。
多线程环境下,肯定要关注线程安全问题。
标准库提供了 PriorityBlockingQueue , 它本身是线程安全的。
package Thread;
import java.util.concurrent.PriorityBlockingQueue;
class MyTask{
//要执行的任务内容
private Runnable runnable;
//任务在啥时候执行,用 ms 时间戳来表示
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();
}
}
class MyTimer{
//扫描线程
private Thread t = null;
//有一个阻塞队列来保存任务
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public MyTimer(){
t = new Thread(()->{
while(true){
//取出队首元素,看看是否到时间了,
//到时间了就执行任务
//没到时间就放回队列中
try {
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if(curTime < myTask.getTime()){
//没到时间
queue.put(myTask);//会触发优先级调整,myTask又回到队首,下次取出的还是这个任务
}else{
//到时间了
myTask.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
//指定 2 个参数
//1.任务内容
//2.任务执行的时间
public void schedule(Runnable runnable,long after){
MyTask myTask = new MyTask(runnable,System.currentTimeMillis()+after);
queue.put(myTask);
}
}
public class ThreadDemo{
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务1");
}
},1000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务2");
}
},2000);
}
}
让 MyTask 类实现 Comparable 接口或使用 Comparator 写个比较器
@Override
public int compareTo(MyTask o) {
//要么是 this.time - o.time,要么是 o.time - this.time.
//到底是哪个,运行看下就知道了
return (int)(this.time - o.time);
}
还有一个问题是:扫描线程里 while(true),取出一个任务时间没到又塞回队列里。一直重复,这种操作成为"忙等"。
等,但是并没有闲着,按理来说,等待是要释放CPU资源的,让CPU去干别的事。但是忙等既进行了等待,又占用CPU资源
举个栗子🌰:比如你定了8:00的闹钟,但是你7:30就醒了,睡一会儿又醒来看还是7:30,睡一会儿又醒来看还是7:30…(是一直在等,但是也没闲着)
像忙等这种情况,需要辩证性的去看待,当前这种情景下 忙等 是不好的, 但是有的情况下忙等是一个好的选择。
针对以上代码,不要再"忙等"了,进行阻塞式等待。
此处的等待是多久?假设当前是1:00,队首元素是2:00,那么等待时间就是1个小时。
此处的等待时间看似明确,其实也并不明确。因为随时都可能有新任务到来(随时可能有线程调用schedule()添加新任务),新任务时间可能更早。
所以这里使用 wait()更合适,更方便随时唤醒,如果有新任务来了,就notify()一下,重新确认队首元素,重新计算需要等待的时间。
带超时时间的 wait() ,可以保证1.如果有新任务来了,随时唤醒。2.如果没有新任务,就等到旧任务的最早时间。
程序里的计时操作,本身就难以做到非常精确,(操作系统调度线程有时间开销的),存在 ms 级别的误差都很正常,也不影响日常使用。如果你的使用场景,就是对时间误差非常敏感,(发射导弹,发射卫星),此时就不能用 windows,Linux 这样的操作系统了,而应该使用像 vxworks 这样的实时操作系统(线程调度是开销极快,可控的,可以保证误差是在要求范围内的)
代码写到这里,还有一个问题,是和线程安全/随机调度相关的。
//多线程 //有 2个线程 //1.线程 t //2.主线程 main
此处只需要把锁的范围放大,放大之后就可以保证执行 notify() 的时候 wait() 已经执行完了。
完整代码:
package Thread;
import java.util.Collections;
import java.util.Comparator;
import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable<MyTask> {
//要执行的任务内容
private Runnable runnable;
//任务在啥时候执行,用 ms 时间戳来表示
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();
}
@Override
public int compareTo(MyTask o) {
//要么是 this.time - o.time,要么是 o.time - this.time.
//到底是哪个,运行看下就知道了
return (int)(this.time - o.time);
}
}
class MyTimer{
//扫描线程
private Thread t = null;
//有一个阻塞队列来保存任务
private PriorityBlockingQueue<MyTask> 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()) {
//没到时间
queue.put(myTask);
this.wait(myTask.getTime() - curTime);
} else {
//到时间了
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
//指定 2 个参数
//1.任务内容
//2.任务执行的时间
public void schedule(Runnable runnable,long after){
MyTask myTask = new MyTask(runnable,System.currentTimeMillis()+after);
queue.put(myTask);
synchronized (this){
this.notify();
}
}
}
public class ThreadDemo{
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务1");
}
},1000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务2");
}
},2000);
}
}
线程池
线程池是什么
因为进程来实现并发编程太"重"了,此时引入了线程,线程的创建,销毁和调度都比进程更高效。此时使用多线程在很多时候就可以代替进程来实现并发编程了。
随着并发程度的提高,随着我们对性能要求标准的提高,发现线程的创建也没有那么轻量。
当我们需要频繁的创建销毁线程的时候,就发现开销还是很大的。
想要进一度的提高这里的效率,有 2 种办法:
1.搞一个"轻量型线程"=>协程/纤程(很遗憾,这个东西目前还没有加入到 Java 标准库).
2.使用线程池,来降低创建/销毁线程的开销。
说到池,可以联想到字符串常量池,数据库连接池…
事先把需要用的线程创建好,放到"池"中,后面需要使用的时候,直接从池里获取,用完了就还给池。
从池子里取和放回池子这比创建/销毁更高效。
创建/销毁线程是由操作系统内核完成的,从池子里取和放回池子这是我们自己用户代码就能实现的,不必交给内核操作
什么是操作系统内核?
在银行大厅里,用户都是自主的。就像程序中的"用户态","用户态"执行的是程序员自己写的代码,这里想干啥,怎么干,都是程序员代码自主决定的。
有些操作需要在银行柜台后完成,你不能进入柜台,需要通过银行的工作人员通过他们来间接完成。就像程序中的"内核态",内核态进行的操作都是在操作系统内核中完成的。内核会给程序提供一些api,称为系统调用,驱使内核完成一些工作。
系统调用里面的内容是直接和内核的代码相关的。这一部分工作不受程序员自主控制,都是内核自行完成的。
相比于内核来说,用户态,程序执行的行为是可控的。想要做某个工作就会非常干净利落的完成。(比如从池子里取/还给池子)。
如果要是通过内核从系统这里创建个线程,就需要通过系统调用,让内核来执行了。此时你不清楚内核身上背负多少任务(内核不是只给你一个应用程序服务的,给所有的程序都要提供服务)
因此,当使用系统调用,执行内核代码的时候,无法确定内核都要做哪些工作,整体过程是"不可控"的。
标准库中的线程池
package thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);//这里的 new 是方法名的一部分,不是 new 关键字。
//创建了一个线程池,池子里线程数目固定是 10 个
}
}
这个操作,使用某个类的某个静态方法直接构造出一个对象来(相当于是把 new 操作给隐藏到静态方法后面了)
像这样的方法,就称为"工厂方法",提供这个工厂方法的类就称为"工厂类"。此处这个代码就使用了"工厂模式"这种设计模式
在当前校招阶段,要研究的设计模式,单例和工厂
工厂模式:使用普通方法来代替构造方法,创建对象。
(为啥要代替?构造方法有坑,坑就体现在只构造一种对象就好办,如果要构造多种不同情况下的对象就难办了)
举个栗子🌰:
想要表示二维平面上一个点,有 2 种办法:平面直角坐标系或极坐标系
class Point{
public Point(double x,double y){
}
public Point(double r,double a){
}
}
但是,很明显这段代码有问题,这 2 个构造方法不符合重载的要求(方法名相同,参数列表不同)
为了解决这个问题就可以使用工厂模式
class PointFactory{
public static Point makePointByXY(double x,double y){
}
public static Point makePointByRA(double r,double a){
}
}
public class ThreadDemo{
public static void main(String[] args) {
Point p = PointFactory.makePointByXY(10,20);
}
}
普通方法,方法名字没有限制,因此有多种方式构造,就可以直接使用不同的方法名即可,此时方法的参数是否要区分已经不重要了。
像工厂模式,对 Python 来说没什么价值,Python 构造方法不像 C++/Java 这么坑,可以直接在构造方法中通过替他手段来做出不同版本的区分。
不同语言,语法规则不一样,因此在不同语言上,能够使用的设计模式可能会不同。有的设计模式已经融合在语言语法内部了。
日常谈到的设计模式,主要是基于 C++/Java/C# 这样的语言来展开的,这里说的设计模式不一定适合其他语言。
ExecutorService pool = Executors.newFixedThreadPool(10);
线程池提供了一个重要的方法,submit,可以给线程池提交若干个任务
运行程序发现,main线程结束了,但是整个进程没有结束。线程池中的线程都是前台线程,此时会阻止进程结束。
前面定时器 Timer 也是同理
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0;i < 1000;i++) {
int n = i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello " + n);
}
});
}
当前是往线程池里放了 1000 个任务,1000 个任务就是这 10 个线程平均分配一下,差不多是一个分配 100 个,这里不是严格的平均,有的多几个,有的少几个都正常。(每个线程执行完一个任务后会立即取下一个任务,由于每个任务执行时间差不多,所以每个线程执行的任务数量也差不多)
进一步的可以认为,这 1000 个任务,就在一个队列里排队呢,这 10 个线程就一次来取队列里的任务,取一个就执行一个,执行完了之后就执行下一个,这个操作很类似做核算…
- 变量捕获
Java当中的匿名内部类, Lambda 表达式中,会存在变量捕获。
run方法属于Runnable,这个方法的执行时机,不是立刻马上,而是在未来的某个节点,后续在线程池的队列中,排到它了就让对应的线程去执行。
i 是主线程里的局部变量,(在主线程的栈上),随着主线程的代码块执行结束就销毁了,很可能主线程的for执行完了,而当前 run 的任务在线程池里还没排到呢,此时 i 就已经要销毁了。
为了避免作用域的差异,导致后序执行 run 的时候 i 已经销毁,于是就有了变量捕获,也就是让run方法把主线程的 i 往当前 run 的栈上拷贝一份(在定义 run 的时候,偷偷把 i 的值记住,后续执行 run 的时候创建一个也叫 i 的局部变量,并把这个值赋值过去)
在 Java 中,对于变量捕获,做了一些额外的要求。在 JDK 1.8 之前,要求变量捕获只能捕获 final 修饰的变量。1.8 之后要求这个变量要么是被final修饰,如果不是被final修饰的你要保证在使用之前,没有修改。
此处的 i 是有修改的,不能捕获,而 n 是没有修改的,虽然没有被 final 修饰但是也能捕获。
上述这些线程池,本质上都是通过包装 ThreadPoolExecutor 来实现出来的
ThreadPoolExecutor 这个线程池用起来更麻烦一点(提供的功能更强大),所以才提供了工厂类,让我们用着更简单。
- java.util.concurrent
这个包里放的很多类都是和并发编程(多线程编程)密切相关的,这个包也简称为 juc
corePoolSize 核心线程数
maximumPoolSize 最大线程数
ThreadPoolExecutor 相当于把里面的线程分成2类:①正式员工(核心线程) ②临时工/实习生
这2个之和是最大线程数。
允许正式员工摸鱼,不允许实习生摸鱼,如果实习生摸鱼太久就会被开除(销毁)
如果任务多,就需要更多的人手(更多的线程),但是一个程序任务不是一直都多,有时候多有时候少。
如果任务少的情况下线程很多就不合适了,需要对现有的线程进行淘汰,整体策略是正式员工保底,实习生动态调节。
实际开发的时候,线程池的线程数,设成多少合适?网上的资料有说:N(CPU 的核数),N + 1, 1.5N ,2N…这些说法都不准确,如果面试中遇到这样的问题,只要你回答出了具体的数字一定就是错的。
不同程序特点不同,此时要设置的线程数也是不同的,
考虑 2 个极端情况:
1.CPU密集型:每个线程要执行的任务都是狂转CPU(进行一系列算术运算),此时线程池的线程数最多也不该超过CPU核数(此时你设置的再大也没用,CPU密集型任务,要一直占用CPU,搞那么多线程,CPU不够了)2.IO密集型:每个线程干的工作就是等待IO(读写硬盘,读写网卡,等待用户输入…),不吃CPU,此时这样的线程处于阻塞状态,不参与CPU调度…此时多搞一些线程都无所谓,不再受制于CPU核数了,理论上来说你线程数设置成无限大都可以(实际上肯定是不行的).
然而实际开发中,并没有程序符合这两种理想模型…真实的程序,往往一部分要吃CPU,一部分要等待IO,具体这个程序几成工作量是吃 CPU的,几成是等待IO的,不确定。
实践中确认线程的数量也非常简单,通过测试/实验的方式。
现代的CPU,是一个物理核心上可以有多个逻辑核心的, 8核16线程,8个物理核心,每个物理核心上有2个逻辑核心,每个逻辑核心同一时刻只能处理一个线程。
long keepAliveTime 是线程池中空闲线程等待工作的超时时间(实习生可以摸鱼的最大时间)
TimeUnit unit 时间单位(s,ms,分钟…)
BlockingQueue<[Runnable> workQueue 线程池的任务队列
此处使用阻塞队列,每个工作线程都是在不停尝试 take ,如果有任务就 take 成功,没有任务就阻塞等待
ThreadFactory threadFactory 用于创建线程的,线程池是需要创建线程的。
RejectedExecutionHandler handler 描述了线程池的"拒绝策略",也是一个特殊的对象,描述了如果线程池任务队列满了,如果再继续添加任务会有什么样的行为。
标准库提供的4个拒绝策略
举个栗子🌰:假设张三最近很忙,快要考试了,张三在准备复习几门课程,这时候室友李四让张三帮他写个作业。
第1种拒绝策略:张三哇的一声哭出来,摆烂了,考试也不复习了,作业也不帮李四写了,(抛出异常)
第2种拒绝策略:张三时间都排满了,没有多的时间帮李四了,让李四自己的作业自己写,继续复习考试
第3种拒绝策略:张三舍己为人,不复习了,帮李四写作业
第4种拒绝策略:张三拒绝帮李四写作业
实现线程池
来实现一个固定数量线程的线程池
一个线程池,至少有2部分:
1.阻塞队列,用来保存任务
2.若干个工作线程
package thread;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool{
//此处不涉及到"时间",此处只有任务,直接使用 Runnable 即可
private BlockingQueue<Runnable> 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();
}
}
//注册任务给线程池
public void submit(Runnable runnable){
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadDemo{
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() {
@Override
public void run() {
System.out.println("hello " + n);
}
});
}
}
}