Java EE 多线程之多线程案例

文章目录

  • 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());
                }
            });
        }
    }
}

在这里插入图片描述

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

V2X在做什么?连接未来智能出行的车联网(上)

来源&#xff1a;德思特测试测量 德思特分享丨V2X在做什么&#xff1f;连接未来智能出行的车联网&#xff08;上&#xff09; 原文链接&#xff1a;德思特分享 | V2X在做什么&#xff1f;连接未来智能出行的车联网&#xff08;上&#xff09; 欢迎关注虹科&#xff0c;为您提…

美易官方:零售销售数据提振信心

美易全球投资中心副总裁Kenny Jolin表示全球股市在经历了动荡之后逐渐恢复了稳定。最近&#xff0c;美国股市表现强劲&#xff0c;连续六天上涨&#xff0c;道琼斯指数也创下了新高。这一趋势不仅反映了投资者信心的恢复&#xff0c;也表明了全球经济正在逐渐复苏。 他说&#…

如何在Centos 7环境下安装MySQL并登录

目录 先获取MySQL官方yum源 然后正常使用yum命令下载mysql即可完成MySQL的下载 使用mysql客户端登录mysqld服务端 能够登录mysql客户端后&#xff0c;我们最后还需要做一点配置 先获取MySQL官方yum源&#xff08;包括对yum源的介绍&#xff09; 介绍一下yum源 yum源就是一…

文献管理器Zotero使用WebDAV结合内网穿透实现公网环境跨平台同步文献笔记

文章目录 一、Zotero安装教程二、群晖NAS WebDAV设置三、Zotero设置四、使用公网地址同步Zotero文献库五、使用永久固定公网地址同步Zotero文献库 Zotero 是一款全能型 文献管理器,可以 存储、管理和引用文献&#xff0c;不但免费&#xff0c;功能还很强大实用。 ​ Zotero 支…

net实践记录

文章目录 前言是否使用继承快捷输入 实体&#xff1b;引用class&#xff0c;提示有保护性System.NullReferenceException:“未将对象引用设置到对象的实例。” 总结 前言 记录使用.net 项目开发过程基础问题记录&#xff0c;便于快速回顾与查询&#xff1b; 是否使用继承 快捷…

关于git clone速度极慢的解决方法

&#xff01;&#xff01;&#xff01;&#xff01;前提条件&#xff1a;得有一个可靠且稳定的梯子&#xff0c;如果没有接下来的就不用看了 前言&#xff1a;我在写这篇文章前&#xff0c;也搜索过很多相关git clone速度很慢的解决方法&#xff0c;但是很多很麻烦&#xff0c…

Maven环境搭建及配置

Maven环境搭建及配置 1.下载部署 官方网站下载正式版的Maven文件,打开bin目录&#xff0c;复制路径然后去环境变量中的path下配置环境变量&#xff0c; 如果只有一个用户只需要在上面path配置复制的路径,当然也可以直接在下面配置,下面配置默认给所有用户都配置 设置完成打开控…

垃圾收集器及内存分配

目录 垃圾收集器种类 HotSpot虚拟机所包含的收集器 垃圾收集器部分源码 垃圾收集器后台日志参数说明与配对关系 1、串行垃圾收集器 串行垃圾收集器运行示意图 1&#xff09;、编写测试代码 2&#xff09;、设置垃圾回收为串行收集器 3&#xff09;、启动程序&#xff…

激活Windows过程及报错解决: 0x803f7001 在运行Microsoft Windows 非核心版本的计算机上, 运行“ slui.exe 0x2a 0x803f7001 “以显示错误文本

激活Windows过程及报错问题解决: 0x803f7001 在运行Microsoft Windows 非核心版本的计算机上&#xff0c;运行“ slui.exe 0x2a 0x803f7001 “以显示错误文本。 前言 最近在激活Windows过程中&#xff0c;遇到了报错: 0x803f7001 在运行Microsoft Windows 非核心版本的计算机上…

超详细 | 哈里斯鹰优化算法原理、实现及其改进与利用(Matlab/Python)

测试函数为F9 在MATLAB中执行程序结果如下&#xff1a; 在Python中执行程序结果如下&#xff1a; 哈里斯鹰优化算法(Harris Hawks Optimization , HHO)是 Heidari等[1]于2019年提出的一种新型元启发式算法&#xff0c;设计灵感来源于哈里斯鹰在捕食猎物过程中的合作行为以及突…

基础算法(3):排序(3)插入排序

1.插入排序实现 插入排序的工作原理是&#xff1a;通过构建有序序列&#xff0c;对于未排序数据&#xff0c;在已经排序的序列从后向前扫描&#xff0c;找到位置并插入&#xff0c;类似于平时打扑克牌时&#xff0c;将牌从大到小排列&#xff0c;每次摸到一张牌就插入到正确的位…

香港威雅报告:香港威雅学校入选英国《优秀学校指南》

今天&#xff0c;我们很荣幸地和大家分享一个特别的消息——香港威雅已接受了英国领先的学校审查机构——《优秀学校指南》&#xff08;The Good Schools Guide&#xff09;的全面评审。这是一家值得信赖的权威评审机构&#xff0c;相关工作人员来访并审查了我们的学校&#xf…

【Monitor, Maintenance Operation, Script code/prgramme】

Summary of M,M&O,Program JD) Monitor & M&O Symbio信必优) Job chance/opportunities on Dec 12th, 20231.1) Content 招聘JD job description:1.2) suggestions from Ms Liang/Winnie on Wechat app1.3) Java微服务是什么&#xff1f;1.3.1) [URL Java 微服务](…

网络互通--三层交换机配置

目录 一、三层交换机的原理 1、概念 2、PC A与不同网段的PC B第一次数据转发过程 3、一次路由&#xff0c;多次转发的概念 4、 三层交换机和路由器的比较 二、利用实验理解交换机 1、建立以下拓扑图​编辑 2、分别配置主机的IP地址&#xff0c;子网掩码、网关等信息 3、…

2DPASS激光雷达点云语义分割简介

导读 香港中文大学深圳深度比特实验室提出了一种基于二维图像先验辅助的激光雷达点云语义分割 (2DPASS)。不同于先前的多模态方法&#xff08;训练和推理阶段均需要成对的图像和点云数据作为输入&#xff09;&#xff0c;该方法仅在训练阶段利用额外的图像数据&#xff0c;从相…

如何关闭微信视频号的详细步骤

随着移动互联网的发展&#xff0c;越来越多的人开始使用各种社交平台来分享自己的生活和工作。而微信作为一款全民级别的应用&#xff0c;自然也不例外。在微信中&#xff0c;除了传统的聊天、朋友圈等功能外&#xff0c;还推出了一个名为“视频号”的功能&#xff0c;可以让用…

54 代码审计-TP5框架审计写法分析及代码追踪

目录 知识点1知识点2演示案例:demo代码段自写和规则写分析hsycms-TP框架-不安全写法-未过滤weipan21-TP框架-规则写法-内置过滤 知识点1 调试&#xff0c;访问&#xff0c;路由&#xff0c;配置&#xff0c;版本等 知识点2 自写写法&#xff1a;自己写代码&#xff0c;一步步…

新闻管理系统

其他项目&#xff0c;点击作者主页 目录 1 系统简介 2 系统相关技术 2.1 Java开发语言 2.1.1 Spring框架 2.1.2 Spring MVC框架 2.1.3 Mybatis框架 2.2 MySQL数据库 3 需求分析 3.1 可行性分析 3.1.1 技术可行性 3.1.2 经济可行性 3.1.3 操作可行性 3.2 业务流…

C语言-实验题目:运动会成绩模拟统计

实验题目&#xff1a;运动会成绩模拟统计 为推动学校体育工作的开展&#xff0c;计算机科学技术学院将在近期举办院运会。本届运动会只设学生组&#xff0c;比赛项目为田径、游泳、篮球、排球、足球、武术、健美操、兵乓球8项。 名次奖励 田径、游泳、健美操、武术 1&#…

【SpringBoot】Starter的使用与案例讲解

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《SpringBoot》。&#x1f3af;&#x1f3af; &…