Java EE 多线程之多线程案例

news2024/12/23 5:21:30

文章目录

  • 1. 多线程案例
    • 1.1 单例模式
      • 1.1.1 饿汉模式
      • 1.1.2 懒汉模式
      • 1.1.3 多线程下的单例模式
    • 1.2 阻塞队列
      • 1.2.1 阻塞队列定义
      • 1.2.2 生产者消费者模型的意义
      • 1.2.4 标准库中的阻塞队列
      • 1.2.5 实现阻塞队列
      • 1.2.6 用阻塞队列实现生产者消费者模型
    • 1.3 实现定时器
      • 1.3.1 标准库中的定时器
      • 1.3.2 自己实现定时器
    • 1.4 线程池
      • 1.4.1 什么是线程池
      • 1.4.2 标准库中的线程池
      • 1.4.3 自己实现简答的线程池

1. 多线程案例

1.1 单例模式

单例模式是校招中最常考的设计模式之⼀

那什么是设计模式呢?
设计模式好⽐象棋中的"棋谱",软件开发中也有很多常⻅的"问题场景".
针对这些问题场景,⼤佬们总结出了⼀些固定的套路,按照这个套路来实现代码,也不会吃亏
设计模式是一种软性规定,遵守设计模式,代码的下限就被兜住了

单例模式就是单个实例(对象)
摸各类,在一个进程中,只应该创建出一个实例,使用单例模式,既可以对代码进行一个更严格的校验和检查

此处介绍两种最基本的实现方式
(1)饿汉模式
(2)懒汉模式

1.1.1 饿汉模式

“饿汉模式”单例模式中一种简单的写法
所谓 “饿” 形容 “非常追切”
实例是在类加载的时候就创建了,创建时机非常早,相当于程序一启动,实例就创建了
就使用 “饿汉” 形容 “创建实例非常迫切,非常早”

//就期望这个类只有唯一一个实例(一个进程中)
class Singleten {
    private static Singleten instance = new Singleten();
    //这个引用,就是我们期望创建出的唯一的实例引用

    public static Singleten getSingleten() {
        return instance;
    }

    private Singleten() {}
}

public class ThreadDemo26 {
    public static void main(String[] args) {
        Singleten s = Singleten.getSingleten();
        Singleten s2 = Singleten.getSingleten();
        System.out.println(s == s2);
    }
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
其他代码想要使用这个类的实力,就需要通过getSingleten 进行获取,不应该在其他代码中重新 new这个对象,而是使用这个方法获取发哦线程的对象
在这里插入图片描述
这个时候其他方法就没法 new ,只能使用 getSingleten

1.1.2 懒汉模式

懒汉模式和饿汉模式相比,创建的时机不太一样
创建实例的时机会更晚,直到第一次使用的时候,才会创建实例

在计算机中,这种懒汉模式很有意义,因为在实际中,加载一个数据可能会很大,但是懒汉模式会只加载一小部分,这样的话会节省不少内存


在创建懒汉模式的时候,先不初始化,先吧初始化设为 null
在这里插入图片描述
在下面进入 if 之后,如果是首次调用 getInstance 实例是null,就会创建新的实例
如果是后续再次调用,那么 instance 不是 null,这样就不会创建新的实例
这样设置,也会保证实例只有一个,同时创建实例的时机就是第一次调用 getInstance
在这里插入图片描述

//懒汉模式实现单例模式
class SingletenLazy {
    private static SingletenLazy instance = null;

    public static SingletenLazy getInstance() {
        if (instance == null) {
            instance = new SingletenLazy();
        }
        return instance;
    }

    private SingletenLazy(){}
}

public class ThreadDemo27 {
    public static void main(String[] args) {
        SingletenLazy s1 = SingletenLazy.getInstance();
        SingletenLazy s2 = SingletenLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

在这里插入图片描述

1.1.3 多线程下的单例模式

在上面的懒汉模式和饿汉模式里面,哪个是线程安全的呢?
在这里插入图片描述
在饿汉模式中,getInstance 直接返回了 Instance 实例,其实只是一个读操作,这是线程安全的
然而在懒汉模式中,在 if 里面读,在new 里面写,这很明显不是线程安全的


懒汉模式详解:
在这里插入图片描述
如图所示,在 t1 线程走到 if 的时候,很有可能 t2 线程也开始,并且也进入到了 t2,那么就会进入 t2 new 一个新的实例,然后接着执行,又 new 了一个对象
这个时候,就有了两个对象,因此会是线程不安全的


那么我们如何让懒汉模式变成线程安全的呢?
这里我们就要用到前面所学的 synchronized

但是要注意,synchronized 必须要写到 if 的外面,因为我们需要把 if 和 new 进行打包,这样在 new 新的实例的时候,才能保证县城安全
在这里插入图片描述

在这里插入图片描述

这样就可以保证,一定是 t1 执行完 new 操作,执行完修改 instance 之后,再回到 t2 执行 if 条件
t2 的 if 条件就不会成立了,t2 直接返回


但是上面的代码还是有问题,因为在创建完第一个实例,后面再次调用 getInstance 就是重复操作,线程本身是不会有现成安全问题的
这个时候每次调用,会让效率很低,因为线程阻塞会导致性能下降,会有很大的影响

这个时候,我们可以在外面套上一层 if ,用来判断我是否需要加锁
在这里插入图片描述
这样两个 if 的代码在多线程中是很重要的,由于线程的可调度性,如果不加if,线程可能会出现不同的结果
第一层 if 判断的是是否要加锁
第二层 if 判断的是是否要创建对象
这样就可以保证线程安全和执行效率了
这样的代码就被称为“双重校验锁


但是,这个代码还是有问题的
在这里插入图片描述
这就是“指令重排序”,引起的线程问题

什么是“指令重排序”呢?
指令重排序,也是编译器优化的一种方式
就是调整原有的执行顺序,保证逻辑不变的前提下,提高程序的效率

在上述代码中,最容易出现指令重排序的就是 new
在这里插入图片描述
这条指令其实包含三个步骤:

  1. 申请一段内存空间
  2. 在这个内存上调用构造方法,常见出这个实例
  3. 把这个内存地址赋给 Instance 引用变量
    正常情况下是 1 2 3 来执行,但是编译器可能会优化为 1 3 2 的循序来执行
    但是如果在多线程就可能会出现问题
    在这里插入图片描述
    在上图中,t1 只执行了两步,刚给Instance 赋值,这个时候 instance 就已经不是 null 了,但是这个对象依然没有初始化
    所以 进入t2 并不会触发 if 进行加锁,也不会进行堵塞,这样就会直接 return
    但是由于 t1 并没有初始化结束,这个时候使用 instance 里面的属性或者方法,就会出错,导致代码的逻辑出行问题

如果先执行 2 后执行 3,这样的错误就不会出现

那么解决上述问题,我们需要 volatile
volatile 有两个功能:

  1. 保证内存可见性,每次访问变量必须都要重新读取内存,而不会优化到寄存器/缓存中
  2. 禁止指令重排序,针对这个被 volatile 修饰的变量的读写操作相关的只i选哪个,是不能被重排序的
    在这里插入图片描述
    这个时候这个变量的读写操作,就不会进行重排序了

1.2 阻塞队列

1.2.1 阻塞队列定义

阻塞队列,就是基于普通队列做出的扩展
1、阻塞队列是线程安全的
2、阻塞队列具有阻塞特性
(1)如果针对一个已经满了的队列进行入队列,此时入队列操作就会阻塞,一直阻塞到队列不满(其他线程出队列元素)之后
(2)如果针对一个已经空了的队列进行出队列,此时出队列操作就会阻塞,一直阻塞到队列不空(其他线程入队列元素)之后

阻塞队列的用处很大,基于阻塞队列,就可以实现“生产者消费者模型

那什么是“生产者消费者模型”呢?
生产者消费者模型描述的是一种多线程编程的方法
比如三个人 a b c 分工协作,a 负责生产,通过服务器传给 b 和 c ,b 和 c 拿 a 生产的东西进行再加工,那么 a 就是生产者,b 和 c 就是消费者,服务器就相当于“阻塞队列”
假如,a 的生产速度很快,b 和 c 很慢,那么这个时候 a 就需要等待 b 和 c,反之亦然,这个特性就是阻塞队列

1.2.2 生产者消费者模型的意义

生产者消费者模型在实际开发中的意义
1、引入生产者消费者模型,就可以更好的做到“解耦合”
在实际开发中,经常会使用到“分布式系统”,并且通过服务器之间的网络通信,最终完成整个功能
有的时候入口服务器和用户服务器 和 商品服务器关系太密切,就会导致一处崩溃处处崩溃
在这里插入图片描述

这个时候我们就需要使用生产者消费者模型,使用阻塞队列,来降低耦合
在这里插入图片描述
当我们引入阻塞队列,最明显的代价,就是余姚增加机器,引入更多的硬件资源
1)上述的阻塞队列,并非是简单的数据结构,而是基于这个数据结构实现的服务器程序,又被部署到单独的主机上了
2)会导致整个系统的结构更加复杂,需要维护的服务器更多了
3)引入了阻塞队列,经过队列的转发,中间是有一定的开销的,会导致性能下降

2、削峰填谷
当服务器遇到了类似像 “三峡大坝遇到降雨量骤增的” 这样的请求骤增的时候,就会进行“削峰填谷”
这里的“峰”和“谷”都不是长时间持续的,而是短时间出现的


在这里插入图片描述
如果外网的请求突然骤增,那么入口服务器 A 的请求数量就会增加很多,压力就会变大,那么 B 和 C 的压力也会很大

那么为什么,请求多的时候,服务器就会挂掉?
因为,服务器处理每个请求,都是需要消耗硬件资源的(包括不限于,cpu,内存,硬盘,网络带宽)
即使一个请求消耗的资源比较少,但是请求暴增,总的消耗也会急剧增多,这样服务器就无法反应了


当我们引入阻塞队列/消息队列的时候,情况就会发生改变
阻塞队列:是一种数据结构
消息队列:基于阻塞队列实现服务器程序
在这里插入图片描述
这个时候,即使外界请求出现峰值,也是由队列承担请求,后面的依然会按照原来的速度取请求
由于队列只是存储数据,抗压能力是比较强的
但是如果请求不断增加,还是可能会挂的

1.2.4 标准库中的阻塞队列

在 Java 标准库中内置了阻塞队列

• BlockingQueue 是⼀个接⼝,真正实现的类是 LinkedBlockingQueue
• BlockingQueue 下有以下之中类,ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue
• put ⽅法⽤于阻塞式的⼊队列,take ⽤于阻塞式的出队列
• BlockingQueue 也有 offer,poll,peek 等⽅法,但是这些⽅法不带有阻塞特性


 put 入队列

在这里插入图片描述
使用 put 和 offer 一样都是入队列,但是 put 是带有阻塞功能,offer 没有带阻塞(队列满了会返回结果)

 take 出队列

take 方法用来出队列,也是带有阻塞功能的

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
        queue.put("aaa");
        String elem = queue.take();
        System.out.println(elem);
        elem = queue.take();
        System.out.println(elem);
    }

在这里插入图片描述
由于 take 是带阻塞的队列,如果队列中没有值就会阻塞,如上述所示,代码打印一行结汇阻塞

1.2.5 实现阻塞队列

步骤:
1、先实现普通队列
2、再加上线程安全
3、再加上阻塞功能

对于第一步,我们使用数组来实现一个环形队列
在这里插入图片描述
这个时候我们要注意什么时候队列空 和 队列满
(1)浪费一个给子,tail 最多走到 head 的前一个位置
(2)引入 size 变量(常用)

class MyBlockingQueue {
    private String[] elems = null;

    private int head = 0;
    private int tail = 0;
    private int size = 0;

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

    public void put(String elem) {
        if (size >= elems.length) {
            //队列满了
            //后续需要让这个代码能够阻塞
            return;
        }
        //新的元素放到 tail 指向的位置上
        elems[tail] = elem;
        tail++;
        if (tail >= elems.length) {
            tail = 0;
        }
        size++;
    }

    public String take() {
        if (size == 0) {
            //队列空了
            //后续也要让这个代码阻塞
            return null;
        }
        //取出 head 位置的元素并返回
        String elem = elems[head];
        head++;
        if (head >= elems.length) {
            head = 0;
        }
        size--;
        return elem;
    }
}

public class ThreadDemo29 {
    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue(1000);
        queue.put("aaa");
        queue.put("bbb");
        queue.put("ccc");
        queue.put("ddd");

        String elem = "";
        elem = queue.take();
        System.out.println("elem: " + elem);
        elem = queue.take();
        System.out.println("elem: " + elem);
        elem = queue.take();
        System.out.println("elem: " + elem);
        elem = queue.take();
        System.out.println("elem: " + elem);
    }
}

在这里插入图片描述
这个时候我们最基础的队列就写完了


接下来,我们就要引入锁,解决线程安全问题

在这里插入图片描述
在 put 里面,if 下面的操作都是“写”操作,必须要用锁包裹起来
上面的 if 操作也是需要写到锁里面的,如果不写,就会导致队列中多加一个

在这里插入图片描述

	public void put(String elem) {
        //锁加到这里和加到方法上本质是一样的,加到方法上是给 this 加锁,此处是给 locker 加锁
        synchronized (locker) {
            if (size >= elems.length) {
                //队列满了
                //后续需要让这个代码能够阻塞
                return;
            }
            //新的元素放到 tail 指向的位置上
            elems[tail] = elem;
            tail++;
            if (tail >= elems.length) {
                tail = 0;
            }
            size++;
        }
    }

    public String take() {
        String elem = null;
        synchronized (locker) {
            if (size == 0) {
                //队列空了
                //后续也要让这个代码阻塞
                return null;
            }
            //取出 head 位置的元素并返回
            elem = elems[head];
            head++;
            if (head >= elems.length) {
                head = 0;
            }
            size--;
            return elem;
        }
    }
}


接下来,我们来考虑如何阻塞

class MyBlockingQueue {
    private String[] elems = null;

    private int head = 0;
    private int tail = 0;
    private int size = 0;

    //准备一个锁对象
    private Object locker = new Object();

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

    public void put(String elem) throws InterruptedException {
        //锁加到这里和加到方法上本质是一样的,加到方法上是给 this 加锁,此处是给 locker 加锁
        synchronized (locker) {
            if (size >= elems.length) {
                //队列满了
                //后续需要让这个代码能够阻塞
                locker.wait();
            }
            //新的元素放到 tail 指向的位置上
            elems[tail] = elem;
            tail++;
            if (tail >= elems.length) {
                tail = 0;
            }
            size++;

            //入队列成功之后进行唤醒
            locker.notify();
        }
    }

    public String take() throws InterruptedException {
        String elem = null;
        synchronized (locker) {
            if (size == 0) {
                //队列空了
                //后续需要让这个代码阻塞
                locker.wait();
            }
            //取出 head 位置的元素并返回
            elem = elems[head];
            head++;
            if (head >= elems.length) {
                head = 0;
            }
            size--;

            //元素出队列之后,进行唤醒
            locker.notify();
            return elem;
        }
    }
}

wait 要加入到 if 中,也要在 synchronized 里面
队列不满地时候,唤醒,也就是在出队列之后,进行唤醒
队列空了,再出队列,同样也需要阻塞,同样是在另一个队列成功后的线程中唤醒

我们的队列,一定是空或者是满的,不能即空又满


但是,上述代码里面依然存在问题,当 A 线程执行 put ,到了 wait 等待
这个时候 B 线程也执行 put ,到了wait 等待接下来 take 一个数,执行到了 notify
这个时候 A 被唤醒,接着往下走
但是 B 很有可能会被 A 代码下面的 notify 给唤醒
这样就出现了错误
在这里插入图片描述
这个时候,我们仅需把 if 改成 while 即可
if 只能判断一次
一旦程序进入阻塞,再次被唤醒,中间的时间会非常长,会出现变故
这个时候有了变故之后,就难以保证,你的条件是否仍然满足
如果改成 while 之后,意味着,wait 唤醒之后,再判断一次条件
wait 之前判定一次,唤醒之后再判定一次,相当于多做了一步确认操作
如果再次确认,发现队列还是满的,就继续等待

class MyBlockingQueue {
    private String[] elems = null;

    private int head = 0;
    private int tail = 0;
    private int size = 0;

    //准备一个锁对象
    private Object locker = new Object();

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

    public void put(String elem) throws InterruptedException {
        //锁加到这里和加到方法上本质是一样的,加到方法上是给 this 加锁,此处是给 locker 加锁
        synchronized (locker) {
            while (size >= elems.length) {
                //队列满了
                //后续需要让这个代码能够阻塞
                locker.wait();
            }
            //新的元素放到 tail 指向的位置上
            elems[tail] = elem;
            tail++;
            if (tail >= elems.length) {
                tail = 0;
            }
            size++;

            //入队列成功之后进行唤醒
            locker.notify();
        }
    }

    public String take() throws InterruptedException {
        String elem = null;
        synchronized (locker) {
            while (size == 0) {
                //队列空了
                //后续需要让这个代码阻塞
                locker.wait();
            }
            //取出 head 位置的元素并返回
            elem = elems[head];
            head++;
            if (head >= elems.length) {
                head = 0;
            }
            size--;

            //元素出队列之后,进行唤醒
            locker.notify();
            return elem;
        }
    }
}

这个时候就是正确的阻塞队列代码了

1.2.6 用阻塞队列实现生产者消费者模型

public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue(1000);

        //生产者
        Thread t1 = new Thread(() -> {
            int n = 1;
            while (true) {
                try {
                    queue.put(n + "");
                    System.out.println("生产元素 " + n);
                    n++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        //消费者
        Thread t2 = new Thread(() -> {
            while (true) {
                try {
                    String n = queue.take();
                    System.out.println("消费元素 " + n);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t1.start();
        t2.start();
    }

在这里插入图片描述
这里就是一个最简单的生产者模型

在以后得实际开发中,往往还是有多个生产者多个消费者,可有可能这
不简简单单是一个线程,也可能是独立的服务器程序,甚至是一组服务器程序
但是其最核心的还是阻塞队列,使用 synchronized 和 wait/notify 达到线程安全

1.3 实现定时器

定时器是日常开发中常用的组件工具,类似于“闹钟”
设定一个时间,当时间到了的时候,定时器自动的去执行某个逻辑


1.3.1 标准库中的定时器

在我们 java 标准库中是提供了定时器的
在实现定时器之前,我们先来看一下,java 中提供的定时器做了什么

public class ThreadDemo30 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        //用来添加任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                //时间到了之后,要实行的代码
                System.out.println("hello timer 3000");
            }
        },3000);

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

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

        System.out.println("hello main");
    }
}

在这里插入图片描述
这里定义一个 timer 添加多个任务,每个任务同时会带有一个时间
这个进程并没有结束,因为 Timer 里内置了前台线程

使用 timer.cancel(); 可以让线程结束

在这里插入图片描述
在这里插入图片描述

1.3.2 自己实现定时器

这个时候我们想要实现 Timer,里面需要什么内容呢?

1、需要一个线程,负责计算时间,等任务到大合适的时间,这个线程就负责执行
2、需要一个队列/数组,能够保存所有 schedule 进来的任务
这个时候们就要不断的去扫描上述队列的每个元素,到时间再执行
但是如果队列很长,这个开销就会很大

这个时候,我们实现优先级队列会更好
由于每个任务都是有时间的,用优先级队列,就不需要遍历,只看队首元素就可以了

就可以使用标准库提供的 PriorityQueue (线程不安全)
标准库也提供了 PriorityBlockingQueue (线程安全)
推荐使用 PriorityQueue,这个时候可以手动加锁


首先我们先创建一个类
在这里插入图片描述
在这个类里面有一点小小的问题
问题就是,这里面没有比较呀,我们需要在这里实现equal 方法
不过本身 Object 提供了这俩方法的实现,但是有些时候,为了让 hash 表更搞笑,需要重写 equals 和 hashCode


这个时候加上了比较方法,但是怎么比较呢
在这里插入图片描述
这个时候,最好的方法是试一试
在这里插入图片描述


接下来,我们创建好了主线程来进行添加
在这里插入图片描述
但是上面还有一个线程进行扫描
在这里插入图片描述
在这里插入图片描述
这个时候就会导致线程安全问题,我们需要对线程进行加锁

在这里插入图片描述
这个时候我们就加上了锁

但是这里我们思考一个问题,是否能把synchronized 放到 while 的外面呢?
是不可以的,因为在主线程中new 一个 MyTumer 的时候就进入了构造方法,进来如果直接加上锁,while又是死循环,这样就永远也解不了锁了


这个时候我们通过加锁解决了线程安全问题,但是我们还有一个问题“线程饿死

在这里插入图片描述
这里代码执行速度非常快,在解锁之后又会重新加锁,那么就会导致其他线程通过 schedule 想要加锁,但是加不上,就导致了线程饿死

这个时候就要引入 wait

首先我们看队列为空的时候,这个时候我们就需要等待,在添加完元素之后就可以唤醒了
在这里插入图片描述
在这里插入图片描述

接下来,我们再看如果要求唤醒的时间没有到,这个时候也是需要等待的
因为如果没有等待,这个循化会一直执行到时间到,这种代码被称为“忙等”(虽然是在等待,但是cpu一直在运算)
在这里插入图片描述

为了让 cpu 资源可以在别的线程运行的时候可以使用,这个时候我们就可以用 wait
在这里插入图片描述

  1. wait 的过程中,有新的任务来了,wait 就会被唤醒,schedule 有 notify 的
    这个时候需要根据新的任务,重新计算等待的时间,因为不知道这个新的任务是不是最早的任务
  2. wait 过程中,没有新的任务,时间到了,执行之前的这个最早的任务即可

这样我们的代码就可以正常运行了

import java.util.PriorityQueue;

//通过这个类,来描述一个任务
class MyTimerTask implements Comparable<MyTimerTask>{
    //在什么时间点来执行这个任务
    //此处约定这里是一个 ms 级别的时间戳
    private long time;
    //实际任务要执行的代码
    private Runnable runnable;

    public long getTime() {
        return time;
    }

    //delay 期望是一个“相对时间”
    public MyTimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        //计算一下真正要执行任务的绝对时间(使用绝对时间,方便判定任务是否到达时间)
        this.time = System.currentTimeMillis() + delay;
    }

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

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

//通过这个类来表示一个定时器
class MyTimer {
    //负责扫描任务队列,执行任务的线程
    protected Thread t = null;
    //任务队列
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    //创建一个锁对象,此处使用this也可以
    private Object locker = new Object();

    public void schedule(Runnable runnable, long delay) {
        synchronized (locker) {
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);
            //添加新的元素之后,就可以唤醒扫描线程的 wait 了
            locker.notify();
        }
    }

    public void cancel() {
        //结束 t 线程
    }

    //构造方法,创建扫描线程,让扫描线程来完成判定和执行
    public MyTimer() {
        t = new Thread(() -> {
            //扫描线程就需要循环的反复的扫描队首元素,然后判断队首元素是不是时间到了
            //如果时间没到,等待
            //如果时间到了,就执行这个任务并且把这个任务从队列中删掉
            while (true) {
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            //暂不处理
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        //获取到当前的时间
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            //当前时间已经达到了任务时间,可以执行任务了
                            queue.poll();
                            task.run();
                        } else {
                            //当前时间还没到,暂时不执行
                            //这里不能使用 sleep,一方面是不能释放锁,另一方面是会错过别的任务
                            locker.wait(task.getTime() - curTime);
                        }
                    }//解锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class ThreadDemo31 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        },3000);

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

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

在这里插入图片描述

1.4 线程池

1.4.1 什么是线程池

池 是一个非常重要的概念,我们常见的有:常量池、数据库连接池、线程池、进程池、内存池…
其核心内容有两点:
1、提前把要用的对象准备好
2、把用完的对象也不要立即释放,先留着以备下次使用
这样是为了提高效率

进程能够解决并发编程的问题,但是冰饭创建销毁进程,成本太高了,引入了轻量级进程,这就是“线程”
但是如果创建销毁线程的频率进一步提高,这个时候我们就要想办法来优化了

解决方案有两种:

  1. 引入轻量级 线程 -> 也称为纤程/协程
    协程本质,是程序员在用户态代码中进行调度,不是靠内核的调度器调度的,这样就节省了很多调度上的开销
  2. 使用线程池
    把使用的线程提前创建好,用完不直接释放,而是存放起来下次使用,这样就节省了创建/销毁线程的开销
    在这个使用的过程中,并没有真的频繁创建销毁,而只是从线程池里,取线程使用,用完了还给线程池

1.4.2 标准库中的线程池

ThreadPoolExecutor 是标准库中提供的线程池
这个类构造方法有很多参数
在这里插入图片描述

  • corePoolSize 核心线程数
    一个线程池中最少有多少线程(相当于正式员工)
  • maximumPoolSize 最大线程数
    一个线程池中最多有多少线程(相当于正式员工+临时工)
  • keepAliveTime 临时工允许的空闲时间
    如果临时工线程空闲,这个时候会等待一段时间,凡是出现下一时刻任务暴增
  • workQueue 传递任务的阻塞队列
    线程池的内部可以持有很多任务,可以通过阻塞队列进行组织
    传入 BlockingQueue,也可传入 PriorityBlockingQueue(这个带有优先级)
  • threadFactory 创建线程的⼯⼚,参与具体的创建线程⼯作
    这里就要提到工厂模式
    工厂模式,也是一种常见的设计模式,通过专门的“工厂类” / “工厂对象”来创建指定的对象
    在 java 中,有的时候我们想创建两种不同的方法来做一种事情,这个时候我们会用到重载,但有的时候重载也会无能为力
    在这里插入图片描述
    为了解决这种问题,我们就引入了“工厂模式”,使用普通的方法来创建对象,就是把构造方法封装了一层
    在这里插入图片描述
    threadFactory 类里面提供的方法,让方法去封装 new Thread 的操作,并且同时给 Thread 设置一些属性,这样的操作就构成了 threadFactory 线程工厂
  • RejectedExecutionHandler 拒绝策略最重要!!!
    在任务池中,有一个阻塞队列,能够容纳的元素有上限
    当任务队列中已经满了,如果继续往队列中添加任务,那任务池会如何做呢?
    在这里插入图片描述1、AbortPolicy():超过负荷,直接抛出异常
    (新任务、旧任务都不执行)
    2、CallerRunsPolicy():调⽤者负责处理多出来的任务
    (新的任务,由添加任务的线程执行)
    3、DiscardOldestPolicy():丢弃队列中最⽼的任务
    4、DiscardPolicy():丢弃新来的任务
    ThreadPollExceutor 本身用起来比较复杂,因此标准库还提供了另一个版本(Executor),把 ThreadPollExceutor 给封装了起来
    Executor 工厂类,通过这个类创建出来不同的线程对象(在内部把 ThreadPollExceutor 创建好了并且设置了不同的参数)
public class ThreadDemo32 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(4);
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}

在这里插入图片描述
在这里插入图片描述
那什么时候使用 Executor(简单使用),什么时候使用 ThreadPollExceutor(高级定制化) 呢?
由于程序的复杂性,很难直接对线程池的线程数量进行估算,更适合的方式就是通过测试的方式找到合适的线程数目,还是要具体情况具体分析

1.4.3 自己实现简答的线程池

写一个固定数量的线程池

  1. 提供一个构造方法,指定创建多少个线程
  2. 在构造方法中,把这些线程都创建好
  3. 有一个阻塞队列,能够持有要执行的任务
  4. 提供 submit 方法,可以添加新的任务

在这里插入图片描述
当我们的代码写到最后,发现报错了,这里的 i 为什么会错误呢?
这是因为变量捕获,new Runnable 是一个匿名内部类,而run 是一个回调函数,回调函数访问当前外部作用域的变量就是变量捕获
变量捕获的变量不能是一个变化的变量,需要是一个 final 或者事实 final
这里就需要把代码进行改变
此处的 n 就是一个“事实final” 变量
每循环,都是一个新的 n , n 本身没有改变,这样就可以捕获
在这里插入图片描述

class MyThreadPoolExecutor {
    private List<Thread> threadList = new ArrayList<>();

    //这就是一个用来保存任务的队列
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);

    //通过 n 指定创建多少个线程
    public MyThreadPoolExecutor(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                //线程要做的事情就是把任务队列中的任务不停的取出来,并且进行执行
                while (true) {
                    try {
                        //此处的 take 带有阻塞功能
                        //如果队列为空,此处的 take 就会阻塞
                        Runnable runnable = queue.take();
                        //取出一个任务就执行一个任务
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
            threadList.add(t);
        }
    }

    public void submit(Runnable runnable) throws  InterruptedException {
        queue.put(runnable);
    }
}

public class ThreadDemo33 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPoolExecutor executor = new MyThreadPoolExecutor(4);
        for (int i = 0; i < 1000; i++) {
            int n = i;
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行任务" + n + ",当前任务为:" + Thread.currentThread().getName());
                }
            });
        }
    }
}

在这里插入图片描述

这里可以看出,多个线程的执行顺序是不确定的,某个线程去到某个任务了,但是并非立即执行,这个过程中可能另一个线程就插到前面了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1303898.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在接触新的游戏引擎的时候,如何能快速地熟悉并开发出一款新游戏?

引言 大家好&#xff0c;今天分享点个人经验。 有一定编程经验或者游戏开发经验的小伙伴&#xff0c;在接触新的游戏引擎的时候&#xff0c;如何能快速地熟悉并开发出一款新游戏&#xff1f; 利用现成开发框架。 1.什么是开发框架&#xff1f; 开发框架&#xff0c;顾名思…

Slice和MB

Slice 编码帧或场分割成若干个slice独立编码,各Slice之间相互独立,Slice中又有多个宏块构成,各Slice中的宏块个数不一定要一致,可以有以下几种Slice的划分方式: 一个picture一个Slice一个picture多个Slice,每个Slice的宏块数目一致一个picture多个Slice,每个Slice的宏块…

注意力机制和自注意力机制

有很多自己的理解&#xff0c;仅供参考 Attention注意力机制 对于一张图片&#xff0c;我们第一眼看上去&#xff0c;眼睛会首先注意到一些重点的区域&#xff0c;因为这些区域可能包含更多或更重要的信息&#xff0c;这就是注意力机制&#xff0c;我们会把我们的焦点聚焦在比…

cadence中如何在更新原理图封装

cadence中如何在更新原理图封装 一、更改原理图封装 当原理图画好后&#xff0c;如果我们发现某个封装错了&#xff0c;需要改动&#xff0c;需要找到你最初画Library中器件封装文件打开&#xff0c;进行修改&#xff0c;修改好后保存。 二、更新封装 保存好后&#xff0c;…

性能测试:方法、工具与最佳实践

目录 前言 1. 为什么进行性能测试&#xff1f; 2. 性能测试方法 3. 性能测试工具 Apache JMeter: LoadRunner: Gatling: Apache ab (Apache Benchmark): Locust: Tsung: BlazeMeter: K6: Neoload: WebLOAD: 4. 最佳实践 设定明确的性能测试目标&#xff1a; 模…

visual studio 2022 IDE对C++代码反汇编

敲一段代码&#xff0c;在windows电脑儿上&#xff0c;忽然想用visual studio瞧瞧这段代码的汇编长什么样&#xff0c;怎么做呢&#xff1f; 代码有了 #include <stdio.h> void sort(int*,int);int main(){int array[] { 977,1,32,3,99,8,7,5,23,6 };int length int(…

Python 学习笔记之 networkx 使用

介绍 networkx networkx 支持创建简单无向图、有向图和多重图&#xff1b;内置许多标准的图论算法&#xff0c;节点可为任意数据&#xff1b;支持任意的边值维度&#xff0c;功能丰富&#xff0c;简单易用 networkx 中的 Graph Graph 的定义 Graph 是用点和线来刻画离散事物…

react Hooks(useRef、useMemo、useCallback)实现原理

Fiber 上篇文章fiber简单理解记录了react fiber架构&#xff0c;Hooks是基于fiber链表来实现的。阅读以下内容时建议先了解react fiber。 jsx -> render function -> vdom -> fiber树 -> dom vdom 转 fiber 的过程称为 recocile。diff算法就是在recocile这个过程…

国内外聊天AI大比拼,你知道几个?一键了解最火聊天AI应用!

国内类ChatGPT的AI工具一网打尽 2022年&#xff0c;是一个不平凡的一年。ChatGPT迅速崭露头角&#xff0c;成为备受瞩目的热门话题。特别是在OpenAI发布了基于GPT-3.5模型的ChatGPT版本后&#xff0c;这一产品因其卓越的对话能力和广泛的应用潜力&#xff0c;很快引起了大众的…

GoWeb开发框架gin-基础路由使用

文章目录 1.安装并写一个demo2.配置GoLang热加载脚手架3.配合net/http使用完整示例4.返回值4.1String返回值4.2Json返回值4.3struct结构体返回值4.4jsonp返回值4.5XML返回值 5.接收前端传过来的参数5.1get请求传值5.2form表单传值5.3接收结构体5.4路由地址传参 6.完整代码 1.安…

nodejs+vue+微信小程序+python+PHP基于spark的酒店管理系统-计算机毕业设计推荐

通过管理系统进行全面的线上管理。管理者可以通过基于spark的酒店管理系统&#xff0c;全面掌握酒店客房的各项状态&#xff0c;会员通过系统查询酒店客房信息&#xff0c;在线预定。 酒店作为旅游过程必须的设施&#xff0c;也逐渐需需要进行信息化管理&#xff0c;把酒店类型…

python 爬虫 m3u8 视频文件 加密解密 整合mp4

文章目录 一、完整代码二、视频分析1. 认识m3u8文件2. 获取密钥&#xff0c;构建解密器3. 下载ts文件4. 合并ts文件为mp4 三、总结 一、完整代码 完整代码如下&#xff1a; import requests import re import os from tqdm import tqdm from Crypto.Cipher import AES# 创建临…

油田中控室与32台碳储罐之间数据无线传输

二氧化碳强化石油开采技术&#xff0c;须先深入了解石油储层的地质特征和二氧化碳的作用机制。现场有8辆二氧化碳罐装车&#xff0c;每辆罐车上有4台液态二氧化碳储罐&#xff0c;每台罐的尾部都装有一台西门子S7-200 smart PLC。在注入二氧化碳的过程中&#xff0c;中控室S7-1…

json.loads和eval 速度对比

json.loads和eval 速度对比 代码1结果图代码2参考地址 代码1 import json import time import pandas as pddata_sets pd.read_pickle("val_token_id.pandas_pickle") data_sets[str(i) for i in data_sets] starttime.time() [json.loads(i) for i in data_sets] …

企业使用APP自动化测试工具的重要因素

随着移动应用市场的蓬勃发展&#xff0c;企业对高质量、高效率的软件交付提出了更高的要求。在这个背景下&#xff0c;APP自动化测试工具成为了企业不可或缺的一部分。以下是企业采用APP自动化测试工具的关键因素&#xff1a; 1. 快速且可重复的测试执行 自动化测试工具能够快速…

好看的早上问候语早安图片,今天最新唯美温馨祝福语

1、天气冷了&#xff0c;情谊不凉&#xff0c;树叶黄了&#xff0c;思念不忘&#xff0c;问候像一杯热茶&#xff0c;让人暖心&#xff01;祝愿我们与健康平安同行&#xff01;朋友们&#xff0c;大家早上好&#xff01; 2、多一个人牵挂是一种幸福&#xff1b;多一个人相知是一…

【分治】循环赛日程表Python实现

文章目录 [toc]问题描述分治算法示例Python实现 无运动员数量约束循环赛日程表算法示例Python实现 问题描述 设有 n 2 k n 2^{k} n2k个运动员要进行网球循环赛&#xff0c;设计一个满足以下要求的比赛日程表 每个选手必须与其他 n − 1 n - 1 n−1个选手各赛一次每个选手一天…

vue elementui点击按钮新增输入框(点多少次就新增多少个输入框,无限新增)

效果如图&#xff1a; 核心代码&#xff1a; <div v-for"(item,index) in arrayData" :key"item.id">//上面这个是关键代码&#xff0c;所有思路靠这个打通<el-inputtype"input" //除了输入框&#xff0c;还有textarea等placeholder&…

编程实战:自己编写HTTP服务器(系列5:执行后台命令)

系列入口&#xff1a;编程实战&#xff1a;自己编写HTTP服务器&#xff08;系列1&#xff1a;概述和应答&#xff09;-CSDN博客 本文介绍执行后台命令的shell.asp的实现。 目录 一、概述 二、主体代码 三、详解 3.1 参数 一、概述 这个功能就相当于一个终端&#xff0c;不…

商城免费搭建之java商城 java电子商务Spring Cloud+Spring Boot+mybatis+MQ+VR全景+b2b2c 鸿鹄云商

鸿鹄云商 SAAS云产品概述 【SAAS云平台】打造全行业全渠道全场景的SaaS产品&#xff0c;为店铺经营场景提供一体化解决方案&#xff1b;门店经营区域化、网店经营一体化&#xff0c;本地化、全方位、一站式服务&#xff0c;为多门店提供统一运营解决方案&#xff1b;提供丰富多…