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

Java EE 多线程之多线程案例_第1张图片

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

1.1.2 懒汉模式

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

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


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

//懒汉模式实现单例模式
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);
    }
}

Java EE 多线程之多线程案例_第5张图片

1.1.3 多线程下的单例模式

在上面的懒汉模式和饿汉模式里面,哪个是线程安全的呢?
Java EE 多线程之多线程案例_第6张图片
在饿汉模式中,getInstance 直接返回了 Instance 实例,其实只是一个读操作,这是线程安全的
然而在懒汉模式中,在 if 里面读,在new 里面写,这很明显不是线程安全的


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


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

但是要注意,synchronized 必须要写到 if 的外面,因为我们需要把 if 和 new 进行打包,这样在 new 新的实例的时候,才能保证县城安全
Java EE 多线程之多线程案例_第8张图片

Java EE 多线程之多线程案例_第9张图片

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


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

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


但是,这个代码还是有问题的
Java EE 多线程之多线程案例_第11张图片
这就是“指令重排序”,引起的线程问题

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

在上述代码中,最容易出现指令重排序的就是 new
Java EE 多线程之多线程案例_第12张图片
这条指令其实包含三个步骤:

  1. 申请一段内存空间
  2. 在这个内存上调用构造方法,常见出这个实例
  3. 把这个内存地址赋给 Instance 引用变量
    正常情况下是 1 2 3 来执行,但是编译器可能会优化为 1 3 2 的循序来执行
    但是如果在多线程就可能会出现问题
    Java EE 多线程之多线程案例_第13张图片
    在上图中,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、引入生产者消费者模型,就可以更好的做到“解耦合”
在实际开发中,经常会使用到“分布式系统”,并且通过服务器之间的网络通信,最终完成整个功能
有的时候入口服务器和用户服务器 和 商品服务器关系太密切,就会导致一处崩溃处处崩溃
Java EE 多线程之多线程案例_第14张图片

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

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


Java EE 多线程之多线程案例_第16张图片
如果外网的请求突然骤增,那么入口服务器 A 的请求数量就会增加很多,压力就会变大,那么 B 和 C 的压力也会很大

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


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

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

Java EE 多线程之多线程案例_第18张图片
由于 take 是带阻塞的队列,如果队列中没有值就会阻塞,如上述所示,代码打印一行结汇阻塞

1.2.5 实现阻塞队列

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

对于第一步,我们使用数组来实现一个环形队列
Java EE 多线程之多线程案例_第19张图片
这个时候我们要注意什么时候队列空 和 队列满
(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);
    }
}

Java EE 多线程之多线程案例_第20张图片
这个时候我们最基础的队列就写完了


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

Java EE 多线程之多线程案例_第21张图片
在 put 里面,if 下面的操作都是“写”操作,必须要用锁包裹起来
上面的 if 操作也是需要写到锁里面的,如果不写,就会导致队列中多加一个

Java EE 多线程之多线程案例_第22张图片

	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 给唤醒
这样就出现了错误
Java EE 多线程之多线程案例_第23张图片
这个时候,我们仅需把 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();
    }

Java EE 多线程之多线程案例_第24张图片
这里就是一个最简单的生产者模型

在以后得实际开发中,往往还是有多个生产者多个消费者,可有可能这
不简简单单是一个线程,也可能是独立的服务器程序,甚至是一组服务器程序
但是其最核心的还是阻塞队列,使用 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");
    }
}

Java EE 多线程之多线程案例_第25张图片
这里定义一个 timer 添加多个任务,每个任务同时会带有一个时间
这个进程并没有结束,因为 Timer 里内置了前台线程

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

Java EE 多线程之多线程案例_第26张图片
Java EE 多线程之多线程案例_第27张图片

1.3.2 自己实现定时器

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

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

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

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


首先我们先创建一个类
Java EE 多线程之多线程案例_第28张图片
在这个类里面有一点小小的问题
问题就是,这里面没有比较呀,我们需要在这里实现equal 方法
不过本身 Object 提供了这俩方法的实现,但是有些时候,为了让 hash 表更搞笑,需要重写 equals 和 hashCode


这个时候加上了比较方法,但是怎么比较呢
Java EE 多线程之多线程案例_第29张图片
这个时候,最好的方法是试一试
Java EE 多线程之多线程案例_第30张图片


接下来,我们创建好了主线程来进行添加
Java EE 多线程之多线程案例_第31张图片
但是上面还有一个线程进行扫描
Java EE 多线程之多线程案例_第32张图片
Java EE 多线程之多线程案例_第33张图片
这个时候就会导致线程安全问题,我们需要对线程进行加锁

Java EE 多线程之多线程案例_第34张图片
这个时候我们就加上了锁

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


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

Java EE 多线程之多线程案例_第35张图片
这里代码执行速度非常快,在解锁之后又会重新加锁,那么就会导致其他线程通过 schedule 想要加锁,但是加不上,就导致了线程饿死

这个时候就要引入 wait

首先我们看队列为空的时候,这个时候我们就需要等待,在添加完元素之后就可以唤醒了
Java EE 多线程之多线程案例_第36张图片
Java EE 多线程之多线程案例_第37张图片

接下来,我们再看如果要求唤醒的时间没有到,这个时候也是需要等待的
因为如果没有等待,这个循化会一直执行到时间到,这种代码被称为“忙等”(虽然是在等待,但是cpu一直在运算)
Java EE 多线程之多线程案例_第38张图片

为了让 cpu 资源可以在别的线程运行的时候可以使用,这个时候我们就可以用 wait
Java EE 多线程之多线程案例_第39张图片

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

Java EE 多线程之多线程案例_第40张图片

1.4 线程池

1.4.1 什么是线程池

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

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

解决方案有两种:

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

1.4.2 标准库中的线程池

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

  • corePoolSize 核心线程数
    一个线程池中最少有多少线程(相当于正式员工)
  • maximumPoolSize 最大线程数
    一个线程池中最多有多少线程(相当于正式员工+临时工)
  • keepAliveTime 临时工允许的空闲时间
    如果临时工线程空闲,这个时候会等待一段时间,凡是出现下一时刻任务暴增
  • workQueue 传递任务的阻塞队列
    线程池的内部可以持有很多任务,可以通过阻塞队列进行组织
    传入 BlockingQueue,也可传入 PriorityBlockingQueue(这个带有优先级)
  • threadFactory 创建线程的⼯⼚,参与具体的创建线程⼯作
    这里就要提到工厂模式
    工厂模式,也是一种常见的设计模式,通过专门的“工厂类” / “工厂对象”来创建指定的对象
    在 java 中,有的时候我们想创建两种不同的方法来做一种事情,这个时候我们会用到重载,但有的时候重载也会无能为力
    Java EE 多线程之多线程案例_第41张图片
    为了解决这种问题,我们就引入了“工厂模式”,使用普通的方法来创建对象,就是把构造方法封装了一层
    Java EE 多线程之多线程案例_第42张图片
    threadFactory 类里面提供的方法,让方法去封装 new Thread 的操作,并且同时给 Thread 设置一些属性,这样的操作就构成了 threadFactory 线程工厂
  • RejectedExecutionHandler 拒绝策略最重要!!!
    在任务池中,有一个阻塞队列,能够容纳的元素有上限
    当任务队列中已经满了,如果继续往队列中添加任务,那任务池会如何做呢?
    Java EE 多线程之多线程案例_第43张图片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");
            }
        });
    }
}

Java EE 多线程之多线程案例_第44张图片
Java EE 多线程之多线程案例_第45张图片
那什么时候使用 Executor(简单使用),什么时候使用 ThreadPollExceutor(高级定制化) 呢?
由于程序的复杂性,很难直接对线程池的线程数量进行估算,更适合的方式就是通过测试的方式找到合适的线程数目,还是要具体情况具体分析

1.4.3 自己实现简答的线程池

写一个固定数量的线程池

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

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

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

Java EE 多线程之多线程案例_第48张图片

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

你可能感兴趣的:(Java,EE,java-ee,java,开发语言)