✏️作者:银河罐头 系列专栏: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 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 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 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 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 {

//要执行的任务内容

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 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 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);

}

});

}

}

}

推荐阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: