JavaEE初阶4.0

目录

五、生产者消费者模型

[1.0 阻塞队列](#1.0 阻塞队列)

(1)标准库提供的阻塞队列

[(2)capacity 初始容量](#(2)capacity 初始容量)

2.0优势

(1)解耦合

(2)削峰填谷

(3)代价

[3.0 代码实例](#3.0 代码实例)

4.0模拟实现简单的阻塞队列

(1)put方法

(2)take方法

(3)要修改的地方

六、线程池

[1.0 介绍](#1.0 介绍)

(1)常量池

(2)线程池

[2.0 Java标准库里的线程池](#2.0 Java标准库里的线程池)

[​编辑(1) int corePoolSize 核心线程 int maximumPoolSize最大线程数](#编辑(1) int corePoolSize 核心线程 int maximumPoolSize最大线程数)

[(2) keepAliveTime 非核心线程允许空闲的最大时间 TimeUnit 时间的单位](#(2) keepAliveTime 非核心线程允许空闲的最大时间 TimeUnit 时间的单位)

[(3) workqueue 阻塞队列 threadFactory 工厂模式](#(3) workqueue 阻塞队列 threadFactory 工厂模式)

[(4) Rejected ExecutionHandler handler 拒绝策略](#(4) Rejected ExecutionHandler handler 拒绝策略)

七、定时器

[1.0 介绍](#1.0 介绍)

(1)Timer

[(2) schedule()方法](#(2) schedule()方法)

[2.0 模拟实现定时器](#2.0 模拟实现定时器)

[(1) 创建一个类 表示一个任务](#(1) 创建一个类 表示一个任务)

[(2) 使用一些集合类把这个多任务管理起来](#(2) 使用一些集合类把这个多任务管理起来)

[(3) 实现schedule方法](#(3) 实现schedule方法)

[(4) 额外创建一个线程 负责执行队列中的任务](#(4) 额外创建一个线程 负责执行队列中的任务)

[(5) 代码展示](#(5) 代码展示)

(6)优化


五、生产者消费者模型

1.0 阻塞队列

其实就是一种更加复杂的队列 实现的功能是 :

队列为空,尝试出队列,出队列的操作就会阻塞。阻塞到其他线程添加元素为止

队列为满,尝试入队列,入队列的操作就会阻塞。阻塞到其他线程取走元素为止

应用场景:实现 生产者消费者模型

例子:三个人包饺子 如果三个人各包个的 可能会出现都需要用擀面杖的时候(锁)

这个时候 一个人专门负责擀饺子皮 另外两个人包

(1)标准库提供的阻塞队列

Java标准库中的 BlockingQueue类 public interface BlockingQueue<E> extends Queue <e>

其中的两个方法 put方法 入队列 take方法 出队列

(offer和poll当然也能用 但是得put和take才带有阻塞功能)

(2)capacity 初始容量

new LinkedlockingQueue<>(capacity:100); 100初始容量 最大能容纳多少元素

如果不设置capacity 默认是一个非常大的数值........ 实际开发,一般建议大家能够设置上你要求的最大值 否则你的队列可能变得非常大 导致把内存耗尽 产生内存超出范围这样的异常

阻塞队列没有提供一个 阻塞的获取队首元素的操作 直接运行,生产者和消费者两个线程的速度旗鼓相当 所以很难见到阻塞的效果

2.0优势
(1)解耦合

模块和模块之间的关系越小 (不一定是两个线程之间 也可以是两个服务器)

例子: A服务器和B服务器 A请求B服务器 B响应A服务器

如果A直接访问B 此时A和B的耦合度就更高 (编写A代码的时候 多多少少还是有一些B的逻辑)

此时,实现一个阻塞队列 作为A服务器和B服务器的中间人

A和队列交互 B和队列交互 A和B通过队列交互 这样进一步降低了耦合度 (后续的修改维护 成本会降低 况且队列一般不会修改)

(2)削峰填谷

为什么服务器会挂? A和B服务器交互的时候

服务器处理每个请求的时候,都是需要消耗一定的硬件资源的 同时有N个请求,超出了机器硬件资源的上限 此时对应的进程就可能会崩溃 或者操作系统产生卡顿 挂了

A这种上游服务器 干的活简单 单个请求消耗的资源较少 像B这种下游服务器 通常承担更重的任务量 复杂的计算/存储/工作 单个请求消耗的资源数更多

日常工作中 确实会给B这样的角色服务器分配更好的机器 即使如此,也很难保证B承担的访问量能够比A高

这个时候 阻塞队列的重要性就体现出来了

队列服务器 针对单个请求 做的事情也少(存储和转发) 队列服务器往往是可以抗很高的请求量的 这个有点像三峡大坝 发洪水的时候能蓄洪缓冲一些时间 队列服务器也是这样

缓冲了B服务器的压力 等峰值过去了 B仍然消费数据 利用波谷的时间 来赶紧消费之前积压的数据

这个就是 削峰填谷

(3)代价

引入队列之后 整体的结构会更加的复杂

此时 就需要更多的机器 进行部署 生产环境的结构会更复杂 管理起来也更加麻烦

效率会受影响

3.0 代码实例
java 复制代码
import java.util.Queue;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

import static java.lang.Thread.sleep;

public class test06 {
    public static void main(String[] args) {
        BlockingDeque<Integer> queue = new LinkedBlockingDeque<>(100);
        Thread producer = new Thread(() -> {
            int n = 0;
            while (true) {
                try {
                    queue.put(n);
                    System.out.println("生产元素" + n);
                    n++;
                    sleep(100);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }, "producer");

        Thread consumer = new Thread(() -> {
            int n = 0;
            while (true) {
                try {
                    queue.take();
                    System.out.println("消费元素" + n);
                    n++;
                    sleep(100);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }, "consumer");

        producer.start();
        consumer.start();
    }

}
4.0模拟实现简单的阻塞队列

先写一个普通的队列 然后修改为阻塞队列

数组或者链表为底层创建队列 轮转数组(数据结构里面学习过的内容)

首先定义基本的 底层数组 队首 队尾 元素个数啥的 一个初始的构造方法来初始化底层数组

下面是内容的重点 put方法和take方法

put方法要实现的功能: 入队列 如果队列是满的时 产生阻塞

take方法要实现的功能:出队列 如果对列是空的时 产生阻塞

(1)put方法

put方法里面需要加锁(这个是产生阻塞的前提) 当元素个数大于数组长度的是偶 产生阻塞

这里使用的是wait方法 因为并不是要终止线程 之后就是入队列的逻辑了

尾巴里面加入元素 尾巴加一 然后如果尾巴超过了数组长度 尾巴放在开头等等

最后结尾放一个notify方法 用来唤醒take阻塞

(2)take方法

相同与put方法 take方法里面也需要放锁 结尾一个notify方法 用来唤醒put阻塞

下面是我的代码:(有需要修改的地方)

java 复制代码
public class MyBlockingQueue {
    private String[] data = null;

    //队首
    private int head = 0;
    //队尾
    private int tail = 0;
    //元素个数
    private int size = 0;

    public MyBlockingQueue(int capacity){
        data = new String[size];
    }

    public void put(String elem){
        synchronized (this){
        while(size >= data.length){
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        data[tail] = elem;
        tail++;
        if(tail>=data.length){
            tail=0;
        }
        size++;
        this.notify();

        }
    }

    public String take() throws InterruptedException {
        synchronized (this){//加锁是因为这个是阻塞队列  要实现阻塞功能
            while(size==0){//使用while而不用if是因为多次重复检查
                this.wait();
            }
            String ret = data[head];
            head++;
            if(head >= data.length){
                head=0;
            }
            size--;

            this.notify();
            return ret;
        }
    }
}
(3)要修改的地方

对于wait方法逻辑漏洞的修改 (put里面)

wait方法唤醒的时机是 队列不满时(队列满了就要阻塞了)

当其他线程执行成功take的时候

同理take里面是 队列不空的时候,才要唤醒 当其他线程成功put的是时候

最后形成的效果:put里面notify方法唤醒take里面的wait take里面的方法唤醒put里面的wait

上述代码中还有一个关键环节 (wait方法的源码里面有介绍 建议使用while)

以take方法里面的wait为例

正常来说take()方法里面的wait唤醒 是通过另一个线程执行put

另一个线程put成功了 此处的size肯定不是0

但是wait不一定只是被notify唤醒,还可能被interrupt这样的方法中断~

如果使用if作为wait的判定条件 此时就存在wait被提前唤醒的风险 这个时候编译器就会执行wait之后的代码 容易产生逻辑bug

更极端的情况:

3个线程put(1 2 3 )都是因为队列满阻塞了

第四个线程take一下 唤醒了上述的线程1 线程1继续往下执行 此时继续执行 确实会触发notify(本来是给take里面的)此时notify可能会唤醒刚才put的阻塞的2 3这两个线程

此时如果用while循环 即使wait被唤醒 还会再次判定条件 再次进行阻塞

这里的循环的目的是为了 "二次验证" 判定当前这里的条件是否成立

wait之前先判定一次 wait唤醒之后也判定一次(再次确认一下,队列是否不空)

put的阻塞被take唤醒 循环不是一直执行的 只是为了wait之后唤醒之后 再次执行一下条件

最终代码:

java 复制代码
public class test08 {
    public static void main(String[] args) {
        MyBlockingQueue queue= new MyBlockingQueue(1000);

        Thread producer = new Thread(()->{
            int n = 0;
            while(true){
               try{
                   queue.put(n+"");
                   System.out.println("生产元素"+n);
                   n++;
               } catch (Exception e) {
                   throw new RuntimeException(e);
               }
            }
        });

        Thread consumer = new Thread(()->{
            int n = 0;
            while(true){
                try{
                    queue.put(n+"");
                    System.out.println("消费元素"+n);
                    n++;
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });

        producer.start();
        consumer.start();

    }
}

六、线程池

1.0 介绍
(1)常量池

它的核心作用就是 节省内存,避免重复创建相同的值。

常量池就是 Java 的"共享数据仓库",把不变的东西存进去,大家要用就直接拿,不用重复创建

池这个概念可以理解为 备胎(随时可以用)

(2)线程池

最初引入线程的原因:频繁的创建线程,太慢了 随着互联网的发展,随着我们对性能要求更近一步~~ 咱们现在觉得,频繁创建销毁线程,开销有些不能接受了

线程池,就是为了让我们高效的创建销毁线程的

解决方案: 线程池 协程(轻量级线程)

为什么线程池就能更加高效呢?类比一个例子

一个操作系统 == 内核+ 配套的应用程序 这个内核是操作系统的核心功能

给所有的应用程序提供服务

例子:我们去银行里面办理业务的时候 有时候需要复印身份证

这个时候我们可以让柜台工作人员帮忙复印(内核) 还可以去角落的复印机(自己创建的线程池)去复印 哪个更快一点?

直接交给柜台工作人员 工作人员可能同时给10个人打印 也可能在打印中途给女朋友回消息

这个过程是不可控的 而角落的自助复印机是可控的

如果有一段代码 需要进入到内核 由内核负责完成一系列工作 这个过程 不可控 咱们程序员写的代码干预不了 但是线程池 就可以省下应用程序切换到内核中这样运行的开销

2.0 Java标准库里的线程池

ThreadPoolExecutor-->线程池准备好一些线程 让这些线程执行一些任务

核心方法是 submit(Runnable) 通过Runnable描述一段要执行的任务

通过submit将任务放到线程池中 此时线程池里的线程就会执行这样的任务

构造这个ThreadPoolExecutor类的时候 构造方法 比较麻烦(参数有点多)

(1) int corePoolSize 核心线程 int maximumPoolSize最大线程数

Java线程池 里面包含几个线程 是可以动态调整的 任务多的时候 自动扩容更多的线程

任务少的时候 把额外的线程干掉 节省资源

(2) keepAliveTime 非核心线程允许空闲的最大时间 TimeUnit 时间的单位

相当于实习生和正式员工的区别 timeunit是个枚举

(3) workqueue 阻塞队列 threadFactory 工厂模式

线程池本质上来说也是生产者消费者模型 调用submit就是生产任务线程池里的线程就是在消费任

工厂方法模式

构造方法有缺陷 无法重载 此时工厂模式来处理bug

构造方法public Point (double x ,double y)和 构造方法public Point(double a,double b)构成了方法的重载 但是在Java编译器里 有时候不一定能构成重载

工厂方法的核心 就是通过静态方法 把构造对象new的过程 各种属性初始化的过程 封装

提供多组静态方法 实现不同情况的构造

public static Point makePoint makePointByXy(double x,double y)

Point p = new Point(); return p;

public static Point makePoint makePointByXy(double x,doubley)

Point p = new Point(); return p;

(4) Rejected ExecutionHandler handler 拒绝策略

整个线程七个参数中 最重要 最复杂的(面试官考察线程池的参数含义 最想听的就是你对于第七个参数的理解)

submit是把任务添加到任务队列中,任务队列是阻塞队列

队列满了 再添加 阻塞~ 一般不希望程序阻塞太多~ 对于线程池来说,发现入队操作时,队列满了不会真的触发 "入队列操作" 不会真的阻塞 而是执行拒绝策略相关的代码

如果调用submit就阻塞 业务逻辑中的线程调用submit 这个线程要响应用户的请求 阻塞了 用户迟迟拿不到请求的响应 用户等很久 直观上看到的现象就是卡了 与其卡了 不如直接告诉是失败了

拒绝策略涉及到四个类

例子解析:背景 A员工一周的任务安排的满满的 老板B给A员工安排了一项新的任务

AbortPolicy :

知道这个消息后 当着老板的面哭哭了 老板心疼了 非但新的任务干不了 甚至之前任务也取消了

CallerRunsPolicy :

老板B给A员工的submit(submit是调用者线程来调用的 也就是说submit整个方法里的代码 都是在这个线程中执行的 )

A告诉老板说 我忙着呢 B老板你还是自己去干那个任务吧!

B老板想要调用线程池里面的线程(A员工等各种员工) 但是线程池这个时候都有事 所以最后还是B老板自己处理任务 如果按照平时的话 就是B老板扔给线程池 用线程池来解决

.Discard0ldesPolicy :

A丢弃自己任务清单里面比较老的一项 去干B老板布置的任务

.DiscardPolicy :

B老板说 这个给你的任务先不搞了 回头有空再说

Java标准库也提供了另一组类 针对ThreadPoolExecutor 进行了进一步的封装 简化线程池的使用 也是基于工厂模式 相当于线程池的工厂类

Executors.newFixedThreadPool( 4) ; 创建线程数 核心线程数和最大线程数一样~

Execuors.newCatchedPool(); 最大线程数是一个很大的数字 (线程无限增加)

两个使用线程池的代码:

java 复制代码
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test09 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(4);
        threadPool.submit(()->{
            System.out.println("hello");
        });
    }
}
-----------------------------------------------------------------------

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class Test10 {

    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 1000; i++) {
            int id = i;
            threadPool.submit(()->{
            System.out.println("hello"+id+","+Thread.currentThread().getName());
            });

        }
    }
}

七、定时器

1.0 介绍
(1)Timer

Java标准库的定时器 Timer

正常描述任务是Runnable 在定时器这里特殊了一点,把Runnable封装了一下到 TimerTask、

TimerTask是Java提供的一个抽象类,用于定义可以被Timer调度执行的任务

public abstract class TimerTask implement Runnable 核心还是重写run方法

和线程池一样 Timer中也包含前台线程,阻止进程结束

*前台线程:会阻止jvm退出 适用于核心业务逻辑 后台线程不会阻止jvm退出 适用于辅助任务

默认情况下 Thread是前台线程 除非显示设置为daemon

mian线程结束后,Jvm会检查是否有前台线程在运行,如果没有,则退出

(2) schedule()方法

是java.util 类提供的核心方法,用于安排任务在指定时间内执行

timer.schedule(new TimerTask() {重写run方法});

创建了匿名的TimerTask的子类 重写了run方法 new了子类的实例

2.0 模拟实现定时器
(1) 创建一个类 表示一个任务

MyTimerTask类

(2) 使用一些集合类把这个多任务管理起来

ArrayList无法兼顾优先级(当管理多个任务的时候,需要确保,时间最早的任务最先执行 通过

通过遍历的方式 找到时间最早)

queue优先级队列 schedule方法里面的offer方法入优先级队列

(3) 实现schedule方法

把任务添加到队列中即可

(4) 额外创建一个线程 负责执行队列中的任务

MyTimer构造方法里面创建线程

线程池不同 线程池是只要队列不为空,就立即取任务并执行

此处需要看队首元素的时间 是否到了 时间到了 才能执行 时间不到 不能执行

*排序:

compare 是Java提供的接口 用于定义对象的自然排序规则 简单来说 就是让对象具备比较大小的能力 方便排序 comparable自然排序 让对象自身定义比较规则(重写comparaTo方法) comparator自定义排序 外部定义比较规则

需求是能够让时间最小的元素 能够在队首?

return (int) (this.time - o.time) ; 还是 return(int) (o.time-this.time); 做实验

(5) 代码展示

Runnable task long delay:传入任务 还有什么时候执行

System.currentTimeMillis()+delay--->获取当前时刻的时间戳api+过多久执行 返回的也是一个long

忙等:明明是等 却需要消耗大量的cpu资源 忙了一天 又没啥实质性的产出

相当于顾客(任务) 取餐屏(队列) 店员(工作线程) 如果没有新的订单 店员一直工作

最终累瘫 但是实际取餐的效率很低

解决方式: slee() 不好控制时间 像是吃了安眠药 期间无法被唤醒 新的订单就会延迟处理

wait notify() 像是浅睡眠 允许其他线程操作队列 随时可以被notify唤醒

队列不为空 schedule方法唤醒

判断条件的bug:另一种类似忙等的bug

虽然是闲着 但是在时间到来之前 一直在工作 这部分也是需要wait

这个wait必须得有其他线程schedule才能唤醒 实际上 此处应该是时间到 就继续执行

等待时间差(2:00到2:30 等30min)

locker.wait( task.getTime()-System.currentTimeMillis() );

中途被唤醒也没事 多执行一次循环无所谓 主要是怕短时间内的大量循环

这个就体现了wait的好处了 如果是sleep 中途是唤醒不起来了 会一直睡

而且sleep是不会释放锁的 另一个线程执行schedule阻塞在加锁的逻辑上~

(呜呜 考虑的真多 就像是在无数种情况下打败灭霸 势必要多思考 不充分的理解多线程 很容易写的代码就有bug 正因为多线程难 有的语言(js)直接就把多线程干掉了 提供定时器凑合实现一些并发效果的 python也是假的线程)

java 复制代码
import java.util.Comparator;
import java.util.PriorityQueue;

public class test12 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();

        //第一个任务
        timer.schedule(new Runnable(){
            @Override
            public void run(){
                System.out.println("hello 5000");
            }
        },5000);

        timer.schedule(new Runnable(){
            public void run(){
                System.out.println("hello 2000");
            }
        },2000);

        timer.schedule(new Runnable(){
            public void run(){
                System.out.println("hello 1000");
            }
        },1000);
    }

}

class MyTimerTask implements Comparable<MyTimerTask> {
    //lambad表达式会被自动转换为Runnable对象 存储在task变量中等待执行
    private Runnable task;

    //记录要执行的任务时刻
    private long time;

    public MyTimerTask(Runnable task, long time) {
        this.task = task;
        this.time = time;
    }

    public int compareTo(MyTimerTask o) {
        return (int) (this.time - o.time);
    }


    public void run() {
        task.run();
    }

    public long getTime() {
        return time;
    }
}
    //自己实现一个定时器
    class MyTimer {
        private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
        //<MyTimeTask>指定队列中存储的元素类型是MyTimeTask对象

        //直接使用this作为锁对象 也是可以的
        private Object locker = new Object();

        public void schedule(Runnable task, long delay) {
            //加锁  主线程 t线程
            synchronized (locker) {
                //以如队列这个时候为基准
                MyTimerTask timerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);
                queue.offer(timerTask);
                locker.notify();
            }

        }


        public MyTimer() {
            //创建一个线程 负责执行队列中的任务

            //构造方法创建了一个线程
            Thread t = new Thread(() -> {
                try {
                    while (true) {
                        synchronized (locker) {
                            //取出队首元素
                            while (queue.isEmpty()) {
                                locker.wait();
                            }
                            //当前就涉及到多个线程操作同一个队列  所以要加锁
                            MyTimerTask task = queue.peek();
                            if (System.currentTimeMillis()<task.getTime() ) {
                                //当前任务时间 如果比系统时间大  说明任务执行时间未到
                                locker.wait(task.getTime()-System.currentTimeMillis());
                                //continue;
                            }else{
                                //时间到了  执行任务
                                task.run();
                                queue.poll();//从栈里面移除元素
                            }
                        }

                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }

            });
            t.start();
        }
    }





    

标准库提供的Timer和自己写的MyTimer差不多 都是使用一个线程 负责扫描队首元素 并执行的

如果任务少 时间分散 都是无所谓的

如果任务特别多 时间非常集中 一个线程就可能执行不过来

Executors.newScheduledThreadPool( 4) 这样的操作 创建了一个带有线程池的定时器

(6)优化

除了基于 堆(优先级队列)方式来实现的定时器外

还有一种方案是 基于"时间轮"

类似搞个循环队列(数组) 每个元素是一个 时间单位 同时每个元素又是一个链表

每到一个时间单位 光标指向下一个元素 同时把这个元素上对应链表种的任务都执行一遍

优势是性能更高 劣势是时间精度不如堆

更适合任务特别多的情况 堆更适合精度高的情况

多线程初阶总结:

1.线程的原理 进程和线程的关系 2.多线程的使用 Thread类的用法

3.线程安全问题 4.等待通知机制 5.多线程代码案例

感谢大家的支持

更多内容还在加载中...........

如有问题欢迎批评指正,祝大家生活愉快、学习顺利!!!

相关推荐
兮山与2 天前
JavaEE初阶3.0
javaee初阶
兮山与2 个月前
JavaEE初阶2.0
javaee初阶