(一).阻塞队列收尾
在上一章的文章中,介绍到了阻塞队列,最后模拟实现了一个阻塞队列
java
package Thread;
class MyBlockingQueue{
private String[] array=null;
//队头
private int head;
//队尾
private int tail;
//元素个数 用于判断阻塞队列是否满了
private int size=0;
public MyBlockingQueue(int capacity){
array=new String[capacity];
}
//put()方法
public void put(String str){
synchronized (this){ //引入线程安全,加锁
//判断队列满了情况
if (size>=array.length){
return;
}
}
array[tail]=str;
tail++;
size++;
//重置,构成循环
if (tail>=array.length){
tail=0;
}
}
//take()方法
public String take(){
synchronized (this){ //引入线程安全,加锁
//判断队列为空的情况
if (size==0){
return null;
}
}
String str=array[head];
head++;
size--;
if (head==array.length){
head=0;
}
return str;
}
}
public class demo29 {
public static void main(String[] args) {
}
}
但是这个阻塞队列还没有写完,我们并没有实现"阻塞"的效果
对于"阻塞"效果,我们可以想一下。在进行put()的时候,应该什么时候进行阻塞?是不是应该阻塞队列满了的时候进行阻塞,当队列满了的时候,再往队列中插入数据,此时插不进去了,所以要进行阻塞。在进行take()的时候,应该什么时候进行阻塞?是不是应该在阻塞队列为空的时候进行阻塞,当队列为空的时候,再往队列里取数据,此时取不出来了,所以要进行阻塞
所以,我们要在take()和put()方法的if()语句中使用wait()方法,同时wait()要搭配"锁"进行操作
但是只有阻塞还不够,我们要对wait()进行唤醒。当队列满了的时候,此时再往里插入数据,发现插不进去了,所以阻塞等待,wait()在等待的过程中会释放锁,那么这个时候,我进行了一次take()操作,那么此时阻塞队列中又可以进行插入数据了。所以总结来说,我们要在put()和take()方法中使用wait()方法,然后在put()方法中使用notify()方法,来唤醒take()中的wait()方法,在take()方法中使用notify()关键字来唤醒put()中的wait()方法
其实这也不难理解,put()中的notify()是唤醒take()中的wait()方法,原因是只有当队列不为空的时候才能被唤醒,所以只有当put()入队列成功的时候才是队列不为空的情况
take()中的notify()是唤醒put()中的wait()方法,原因是只有当队列不满的时候才能被唤醒,所以当take()出队列成功的时候才是队列不为空的情况
java
package Thread;
class MyBlockingqueue{
String[] array=null;
private int head=0;
private int tail=0;
private int size=0;
public MyBlockingqueue(int capacity){
array=new String[capacity];
}
public void put(String str) {
synchronized (this){
//判断队列是否满了
if (size==array.length){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
array[tail]=str;
tail++;
size++;
if (tail>=array.length){
tail=0;
}
this.notify();
}
}
public String take() {
synchronized (this){
//判断队列是否为空
if (size==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String ret=array[head];
head++;
size--;
if (head>=array.length){
head=0;
}
this.notify();
return ret;
}
}
}
public class demo30 {
public static void main(String[] args) {
}
}
此时,基本的阻塞队列已经完成了,但是这里还有一个问题
java
if (size==array.length){
this.wait();
}
这里我们用的是一个if()判断,当我们点开wait()的原码的时候

我们发现,编译器建议我们用while()循环,这是为什么?
这是因为,wait()方法除了能被notify()方法给唤醒,还可能被interrupt()方法在中断的时候将wait()方法给唤醒,如果被interrupt()唤醒,那么当处理异常的时候继续往下走,然后执行tail++等操作,但是一旦执行,那么程序就出问题了,因为tail就越界了,所以搭配while()循环进行使用,目的是为了"二次验证",判断一下当前条件是否成了,wait()之前判断一次,wait()唤醒之后再判断一次
java
package Thread;
class MyBlockingqueue{
String[] array=null;
private int head=0;
private int tail=0;
private int size=0;
public MyBlockingqueue(int capacity){
array=new String[capacity];
}
public void put(String str) {
synchronized (this){
//判断队列是否满了
while (size==array.length){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
array[tail]=str;
tail++;
size++;
if (tail>=array.length){
tail=0;
}
this.notify();
}
}
public String take() {
synchronized (this){
//判断队列是否为空
while (size==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String ret=array[head];
head++;
size--;
if (head>=array.length){
head=0;
}
this.notify();
return ret;
}
}
}
public class demo30 {
public static void main(String[] args) {
}
}
注意:如果3个线程都在put()中阻塞了,此时线程4 take()了一下,然后随机唤醒了线程1,那么线程1继续往下执行,当线程1在put()方法中执行notify()的时候,可能会唤醒线程2或者线程3(小概率)
(二).线程池
1.概念
线程池可以理解为把线程提前创建好,放到一个池子(类似于数组)中,用的时候随时去取,用完了之后放回到池子中。
线程池是为了让我们能够高效的创建和销毁线程。随着互联网的发现,我们对性能需求更进一步,我们觉得频繁的创建销毁线程的开销有些不能接收,所以此时就会有两个方案:①.线程池②.协程(轻量化线程) ②目前在java圈子里还没有普遍使用,所以这里只介绍线程池
2.为什么从池子里直接取线程会比直接创建线程的开销更大?
想要了解为什么,我们需要先了解 操作系统中的 "用户态" 和 "内核态"
一个操作系统 = 内核 + 配套的应用程序
内核的作用就是 "管理硬件设备和给软件提供稳定的运行环境",一个操作系统中内核就是一份,一份内核,要给所有的应用程序提供服务支持
从线程池中取现成的线程是纯应用程序代码就可以完成,所以是一个可控的;从操作系统创建线程,就需要挫折系统内核配合完成,所以是一个不可控的。
使用线程池,就可以省下应用程序切换到"内核"中运行的这样的开销
3.线程池构造的介绍
(1).引入
Java 标准库中也提供了直接使用的线程池 ThreadPoolExcutor
其中,核心方法为submit(Runnable runnable)方法,通过runnable描述一段要执行的任务,通过submit()方法将任务放到线程池中,此时线程池里的线程就会执行这样的任务
(2).参数介绍

有四个构造方法,最后一个构造方法的参数最多,所以直接介绍最后一个构造方法
java
// ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(
// int corePoolSize,
// int maximumPoolSize,
// long keepAliveTime,
// TimeUnit unit,
// BlockingQueue<Runnable> workQueue,
// ThreadFactory threadFactory,
// RejectedExecutionHandler handler);
corePoolSize:"核心线程数",表示线程池中最少有多少个线程,线程池一旦创建,这些线程也就随之创建了,直到这个线程池销毁,这些线程也就随之销毁了
maximumPoolSize:"最大线程数","核心线程"+"非核心线程",对于非核心线程,不繁忙就销毁了,繁忙之后再创建
keepAliveTime:"非核心线程允许空闲的最大时间"
unit:"时间单位",枚举类型 second 表示 "秒"
workQueue:"工作队列",对于我们来说,可以自由的选择数组/链表,指定capacity,是否需要带优先级/比较规则 的工作队列,线程池本质上也是 "生产者消费者模型",调用submit()就是生产任务,线程池里的线程就是在消费任务
当然Java开发文档也提供了一个默认的工作队列**"defaultThreadFactory()"**
threadFactory:"工厂模式",工厂模式主要用于弥补构造方法的缺陷,和"单例模式"是并列的关系,也是一种设计模式。
工厂设计模式主要是通过静态方法,把构造对象new的过程以及各种属性的初始化的过程封装起来了,后续如果使用到了这里的对象,则直接通过静态方法的调用,就可以获取到了
示例:在高中的时候我们学过求一个点的横坐标和纵坐标
方法1:直接写出x和y
方法2:通过极坐标,从坐标原点O到所求的点连一条直线r,然后知道了r和x轴的夹角@,那么x=r*cos(@) ,y=r*sin(@)

针对上图中的情况,构造方法名字是固定的,想要提供不同的版本就需要通过重载,但是有的时候还不一定能过能够构成重载,那么这个时候"工厂模式"这个设计模式就可以解决该问题

hander:"拒绝策略"
对于"阻塞队列"来说,submit()把任务添加到任务队列中,如果队列满了,再添加,那么就会发生阻塞,但是对于"线程池"来说,发现入队列操作时,队列满了,不会真的触发"入队列",不会真阻塞,而是执行 "拒绝策略"相关的任务
"拒绝策略"有四种形式

①.AboutPolicy:线程池直接抛出异常,会导致线程池可能无法继续工作
②.CallerRunsPolicy:让调用submit()的线程自行执行任务
③.DiscardOldestPolicy:丢弃队列中最老的任务
④.DiscardPolicy:丢弃最新的任务,即当前的submit()的这个任务
4.线程池的进一步封装
对于ThreadPoolExcutor,用户觉得需要传的参数太多了,所以Java标准库针对ThreadPoolExcutor进行了进一步封装,简化了线程的使用,同时也是基于工厂设计模式
Executors
①.通过 newFixedThreadPool

②.通过 newCachedThreadPool

注意:在使用线程池的时候,建议使用ThreadPoolExcutor这个版本,不应该使用Executors,因为Executors的线程数目/拒绝策略等信息都是隐式的,可能会不好控制
5.模拟实现线程池
在模拟实现线程池的时候,我们首先要创建出指定数量的线程,所以说在构造方法中,就需要把线程创建出来。同时要注意,随时都会有新的任务被添加进去,所以说线程就需要持续不断的尝试读取新的任务,如果线程取到了任务,那么就执行,如果没取到,就阻塞等待
java
package Thread;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool{
public BlockingQueue<Runnable> blockingQueue=null;
//初始化线程
public MyThreadPool(int n){
//初始化任务队列
blockingQueue=new LinkedBlockingQueue<>();
//创建n个线程
for (int i = 0; i < n; i++) { //只负责创建出n个线程
Thread thread=new Thread(()->{ //对应的线程执行while(true)
while (true){
try {
//从阻塞队列中取任务
Runnable runnable=blockingQueue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
}
public void submit(Runnable runnable) throws InterruptedException {
//将任务放到阻塞队列中
blockingQueue.put(runnable);
}
}
public class demo33 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool=new MyThreadPool(4);//创建4个线程
for (int i = 0; i < 100; i++) {
int id=i;
myThreadPool.submit(()->{ //重写run方法
System.out.println("hello"+" "+id+" "+Thread.currentThread().getName());
});
}
}
}
6.shutdown()方法

可以看到,不管是我们自己模拟实现的线程池还是Java开发文档自带的线程池,当把所有的任务都处理完之后,程序并没有结束,这是因为,这些线程一直还在阻塞等待,等待传入新的任务。
如果想要将线程池里面的线程全部关掉,那么就可以使用**shutdown()**方法
注意:shutdown()方法虽然能将所有的线程关掉,但是不能保证线程池里面的所有任务一定能全部能执行完毕。所以如果需要等待线程池里面的所有任务都执行完毕,需要调用awaitTermination()方法