详细讲解 —— 多线程的四个案例、单例模式、阻塞队列、定时器、线程池(Java EE初阶)(万字长文)

多线程

  • 1 wait和notify
      • 1.1 wait()方法
      • 1.2 notify()方法 \ notifyAll()方法
      • 1.3 wait 和 sleep 的对比(面试题)
  • 2. 多线程案例
      • 2.1 单例模式(经典面试题)
        • 2.1.1 饿汉模式
        • 2.1.2 懒汉模式
        • 2.1.3 懒汉模式(线程安全版)
        • 2.1.4 懒汉模式(线程安全改版)
        • 2.1.5 懒汉模式(线程安全最终版本)
      • 2.2 阻塞队列
        • 2.2.1 阻塞队列是什么
        • 2.2.2 生产者消费者模型是什么
        • 2.2.3 阻塞队列的优点
        • 2.2.4 标准库中的阻塞队列
        • 2.2.5 实现阻塞队列
        • 2.2.6 测试阻塞队列
      • 2.3 定时器
        • 2.3.1 标准库中的定时器
        • 2.3.2 实现定时器
        • 2.3.3 完整代码
      • 2.4 线程池
        • 2.4.1 线程放到线程池中,为什么申请释放更快了
        • 2.4.2 Java标准库中的线程池
        • 2.4.3 实现线程池

1 wait和notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序

join也是控制顺序的方式,这种方式更倾向于控制线程结束。

完成这个协调工作, 主要涉及到三个方法

  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.

wait(),notify()和notifyAll()都是Object类的方法,所以每一个对象都有这三个方法。

1.1 wait()方法

public class TestDome4 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("等待前:");
        object.wait();
        System.out.println("等待后:");
    }
}

上面这个代码会出现异常。异常如下:
详细讲解 —— 多线程的四个案例、单例模式、阻塞队列、定时器、线程池(Java EE初阶)(万字长文)_第1张图片
上面的异常是非法的锁状态异常。

wait 做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁.

wait 要搭配 synchronized 来使用。脱离 synchronized 使用, wait 就会直接抛出异常(就像上面代码一样抛出IllegalMonitorStateException)。

public class TestDome4 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object){
            System.out.println("等待前:");
            object.wait();   //一直等待notify方法。
            System.out.println("等待后:");
        }
    }
}

详细讲解 —— 多线程的四个案例、单例模式、阻塞队列、定时器、线程池(Java EE初阶)(万字长文)_第2张图片

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本,来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出InterruptedException 异常.

1.2 notify()方法 \ notifyAll()方法

1)notify()

notify 方法是唤醒等待的线程。

public class TestDome5 {
    public static void main(String[] args) {
        Object object = new Object();
        Thread thread1 = new Thread(()->{
            synchronized (object){
                System.out.println("thread1 wait 前");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread1 wait 后");
            }
        });
        thread1.start();

        Thread thread2 = new Thread(()->{
            synchronized (object){
                System.out.println("thread2 notify 前");
                object.notify();
                System.out.println("thread2 notify 后");
            }
        });
        thread2.start();
    }
}

详细讲解 —— 多线程的四个案例、单例模式、阻塞队列、定时器、线程池(Java EE初阶)(万字长文)_第3张图片
notify()唤醒wait()的实例。

2)notifyAll()

notify方法只是唤醒某一个等待线程。使用notifyAll方法可以一次唤醒所有的等待线程。
如果把所有的线程都唤醒,唤醒的几个线程就会抢占式执行。

1.3 wait 和 sleep 的对比(面试题)

其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻
塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间.

  1. wait 需要搭配 synchronized 使用,sleep 不需要。
  2. wait 是 Object 的方法 sleep 是 Thread 的静态方法。

2. 多线程案例

2.1 单例模式(经典面试题)

实现一个线程安全的单例模式,单例模式是设计模式之一。

什么是设计模式

设计模式,就是一些固定的代码套路。
我们写代码,有很多经典场景,在这些经典的场景中,有一些经典的对策手段。一些 Java 语言的设计者,把这些常见的应对手段,给收集整理起来,起了一个名字,就叫 “设计模式” 。

这些设计模式,可以让程序员(就算是一个新手程序员),不至于把代码写的太差。

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。
这一点在很多场景上都需要,比如 JDBC 中的 DataSource 实例就只需要一个。

单例模式具体的实现方式,分成 “饿汉” 和 “懒汉” 两种。

2.1.1 饿汉模式

饿汉的单例模式,是比较着急的创建实例的。(程序中用到了这个类,就会立即创建)

class Singleton{
    //1. 使用static创建一个实例,并立即进行实例化,这个instance对应的实例,就是该类的唯一实例。
    //   使用private修饰权限,防止对象调用这个成员变量。
    private static Singleton instance = new Singleton();
    //2. 为了防止程序员在其他的地方不小心的new了这个Singleton,就可以把构造方法的权限设置成private。
    private Singleton(){};
    //3. 提供一个方法,让外面能拿到唯一的实例。
    public static Singleton getInstance(){
        return instance;
    }
}

在饿汉模式中getInstance这个函数,仅仅是读取了变量的内容,如果多个线程只是读同一个变量,但是不修改,此时线程是安全的。

2.1.2 懒汉模式

懒汉的单例模式,是不太着急的去创建实例,只有在用到的时候,才去真正的创建。

class Singleton2{
    //1. 不是立即初始化实例。
    private static Singleton2 instance = null;
    //2. 把构造方法的权限设置成private。
    private Singleton2(){}
    //3. 提供一个方法来获取上面的单例模式的实例。
    //   只有用到这个实例的时候,才会去真正的创建这个实例。
    public static Singleton2 getInstance(){
        if(instance == null){
            instance = new Singleton2();
        }
        return instance;
    }
}

懒汉模式中,既包含了读,又包含了修改。
而且这里的读和修改,还是分成两个步骤的(不是原子的)。存在线程安全问题。

为了结果线程安全的问题,就要加锁。

2.1.3 懒汉模式(线程安全版)

class Singleton2{
    //1. 不是立即初始化实例。
    private static Singleton2 instance = null;
    //2. 把构造方法的权限设置成private。
    private Singleton2(){}
    //3. 提供一个方法来获取上面的单例模式的实例。
    //   只有用到这个实例的时候,才会去真正的创建这个实例。
    public static Singleton2 getInstance(){
        synchronized (Singleton2.class){
            if(instance == null){
                instance = new Singleton2();
            }
        }
        return instance;
    }
}

上面的代码使用类对象作为锁对象,类对象在一个程序中只有唯一的一个,这样就能保证调用getInstance的时候都是针对同一个对象进行加锁的。

上面的代码又出现了问题。

对于第一个懒汉模式的代码来说,线程不安全是发生在instance被初始化之前,未初始化的时候,多线程调用getInstance方法,可能同时涉及到读和修改,但是一旦instance被初始化之后,getInstance方法就只涉及到读操作了,不涉及修改操作,所以被初始化之后线程安全了。

而按照上面加锁方式执行代码,无论代码是初始化之后,还是初始化之前,每次调用getInstance方法,都要进行加锁,也就意味着即使是初始化之后,线程安全了,也存在大量的锁竞争。

2.1.4 懒汉模式(线程安全改版)

改进方案,让getInstance初始化之前,才进行加锁,初始化之后就不进行加锁了。
实现上面的条件,就要先判定instance是否初始化,如果初始化了就不进行加锁了,如果没有初始化就不进行加锁。

class Singleton2{
    //1. 不是立即初始化实例。
    private static Singleton2 instance = null;
    //2. 把构造方法的权限设置成private。
    private Singleton2(){}
    //3. 提供一个方法来获取上面的单例模式的实例。
    //   只有用到这个实例的时候,才会去真正的创建这个实例。
    public static Singleton2 getInstance(){
        if(instance == null){   //判断是否初始化了,如果没有初始化就加锁,如果初始化就不加锁。
            synchronized (Singleton2.class){
                if(instance == null){   //判断是否要创建实例。
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}

上面的两个条件一模一样,完全是一个巧合,他们的作用完全不一样,第一个if是判断是否初始化了,第二个if是判断是否要创建实例。

当前的代码中还存在一个重要的问题。

如果是很多的线程同时都去调用这里的getInstance函数,就会造成大量的度instance内存的操作,这样就可能造成编译器优化,直接读寄存器。(内存可见性问题)

2.1.5 懒汉模式(线程安全最终版本)

内存可见性问题,可能会引起第一个if判定失败,但是对于第二个if条件不会判定失败,因为第二个if条件在synchronized里面,synchronized能保证内存可见性,也能保证原子性,所以第二个if读的是正确的值,但是第一个if可能误判。

为了解决上述问题,就需要使用volatile这个关键字来保证内存的可见性。

class Singleton2{
    //1. 不是立即初始化实例。
    private volatile static Singleton2 instance = null;
    //2. 把构造方法的权限设置成private。
    private Singleton2(){}
    //3. 提供一个方法来获取上面的单例模式的实例。
    //   只有用到这个实例的时候,才会去真正的创建这个实例。
    public static Singleton2 getInstance(){
        if(instance == null){
            synchronized (Singleton2.class){
                if(instance == null){
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}

2.2 阻塞队列

2.2.1 阻塞队列是什么

阻塞队列是一种特殊的队列,遵守着**“先进先出”**的原则。

阻塞队列是一种线程安全的数据结构, 并且具有以下特性:

  • 当队列满的时候, 继续入队列就会阻塞,直到有其他线程从队列中取走元素。
  • 当队列空的时候, 继续出队列就会阻塞,直到有其他线程往队列中插入元素。

阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型。

2.2.2 生产者消费者模型是什么

我们用一个例子(包饺子)来解释生产者消费者模型。

假设有A B C三个人一起来包饺子。

  1. A B C分别每个人都是先擀一个圆形的饺子皮,然后再包饺子。(这样存在一定的问题,擀面杖是只有一个的,这个竞争擀面杖就比较激烈)
  2. A 专门负责擀饺子皮,B 和 C 专门负责包饺子。
    A就是生产者,生产饺子皮,B就是消费者,消费饺子皮,生产的饺子皮要放在一个圆盘上面,这个圆盘就是 交易场所。

2.2.3 阻塞队列的优点

生产者消费者模型,是实际开发中比较常见的场景,阻塞队列是解决这个场景非常有用的手段。尤其在服务器开发的场景中。

假设,有两个服务器,A B,A作为入口服务器直接接收用户的网络请求,B作为应用服务器,来给A提供一些数据。

优点1:能够让多个服务器程序之间更充分的解耦合

详细讲解 —— 多线程的四个案例、单例模式、阻塞队列、定时器、线程池(Java EE初阶)(万字长文)_第4张图片
就像上图,如果不使用阻塞队列,这个时候A 和 B 的耦合性是比较强的。

在开发A 代码的时候就要充分了解到 B 提供的一些接口。在开发 B 代码的时候也要充分了解 A 是怎么调用的,一旦想把 B 换成 C ,A代码就需要较大的改动。而且如果 B 挂了,就可能导致 A 也顺带挂了。

如果使用了阻塞队列,就可以降低这里面的耦合性。

详细讲解 —— 多线程的四个案例、单例模式、阻塞队列、定时器、线程池(Java EE初阶)(万字长文)_第5张图片
上图:对于请求,A是生产者,B是消费者。对于响应,A是消费者,B是生产者。而阻塞队列是作为交易场所。

A只需要关注如何和队列交互,不需要认识B。B也只需要关注如何和队列交互了,不需要认识A,但是队列是不变的。如果B挂了,对于A没有什么影响,如果把 B 换成 C ,A也没有什么影响。

优点2:能够对于请求进行“削峰填谷”

详细讲解 —— 多线程的四个案例、单例模式、阻塞队列、定时器、线程池(Java EE初阶)(万字长文)_第6张图片
未使用生产者消费者模型(阻塞队列),如果 A 请求量突然的暴涨,B的处理也就暴涨,计算量暴涨,如果请求量太大,就需要更多的硬件资源,如果资源不够了,可能程序就挂了。

详细讲解 —— 多线程的四个案例、单例模式、阻塞队列、定时器、线程池(Java EE初阶)(万字长文)_第7张图片
使用阻塞队列,A 的请求增多,阻塞队列的请求增多,阻塞队列只是存数据,没有太大的计算量,就能抗住更大的压力。
B 这边还是按照之前的速度来处理和消费数据,不会因为A的暴涨而引起暴涨,B被保护的很好,就不会出现请求过多所引起程序崩溃。

削峰 —— 请求过多往往是因为一段时间的请求过多,不会持续太长的时间。
填谷 —— B仍然按照之前的速度处理到来的数据。

在我们的生活中,也有这种“削峰填谷”,就比如“三峡大坝”,在雨水多的季节,储存水量,在旱季,释放水量。

2.2.4 标准库中的阻塞队列

在Java标准库中内置了阻塞队列,如果我们需要在一些程序中使用阻塞队列,就直接使用标准库中的就可以了。

  1. BlockingQueue 是一个接口,真正实现的类是 LinkedBlockingQueue
  2. put方法用于阻塞的入队列,take方法用于阻塞式的出队列
  3. BlockingQueue 也有 offer,poll,peek等方法,但是这些方法不带有阻塞特性
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

public class TestDome3 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new LinkedBlockingDeque();
        queue.put("212343215");
        String str = queue.take();
    }
}

2.2.5 实现阻塞队列

实现阻塞队列需要三个步骤,第一步实现一个普通的队列,第二步加上线程安全,第三步加上阻塞。

实现普通的队列

我们这里实现的普通队列是基于数组实现的(循环队列)。

class MyBlockingQueue{
    private int[] queue = new int[1000];
    private int front = 0;
    private int rear = 0;
    private int size = 0;

    public void put(int num){
        if(this.size == queue.length){
            //队列满了
            return ;
        }
        this.queue[this.rear] = num;
        this.rear++;
        if(this.rear >= queue.length){
            rear = 0;
        }
        this.size++;
    }
    public int take(){
        if(this.size == 0){
            //队列空了
            return 0;
        }
        int ret = this.queue[this.front];
        this.front++;
        if(this.front >= queue.length){
            front = 0;
        }
        this.size--;
        return ret;
    }
}

实现线程安全和阻塞效果

保证多线程环境下,调用这里的put和take都没有问题。put 和 take 里面的代码都是在操作公共的变量,所以都是要加锁的。

实现堵塞的要点是,使用wait和notify机制。对于put来说,阻塞条件就是队列为满。对于take来说,阻塞条件,就是队列为空。

class MyBlockingQueue{

    private int[] queue = new int[1000];
    private int front = 0;
    private int rear = 0;
    private int size = 0;
    private Object locked = new Object();

    public void put(int num) throws InterruptedException {
        synchronized (locked){    //加锁保证原子性
            if(this.size == queue.length){
                //队列满了
                locked.wait();   //如果队列满了,就等待take释放空间。
            }
            this.queue[this.rear] = num;   //添加一个元素
            this.rear++;
            if(this.rear >= queue.length){
                rear = 0;
            }
            this.size++;
            locked.notify();   //唤醒take中队列为空时候的等待
        }
    }

    public int take() throws InterruptedException {
        synchronized (locked){
            if(this.size == 0){
                //队列空了
                locked.wait();   //如果队列为空,就等待put添加元素。
            }
            int ret = this.queue[this.front]; 
            this.front++;   //删除一个元素
            if(this.front >= queue.length){
                front = 0;
            }
            this.size--;
            locked.notify();   //唤醒take中队列为满时候的等待
            return ret;
        }
    }
}

2.2.6 测试阻塞队列

public class TestDome4 {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue queue = new MyBlockingQueue();
        Thread customer = new Thread(()->{    //消费者线程
            while(true){
                try {
                    //Thread.sleep(500);
                    int value = queue.take();
                    System.out.println("消费了:" + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();

        Thread producer = new Thread(()->{    //生产者线程
            int num = 0;
            while(true){
                try {
                    Thread.sleep(500);
                    queue.put(num);
                    System.out.println("生产了:" + num);
                    num++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();

        customer.join();
        producer.join();
    }
}

2.3 定时器

定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.

join(指定超时时间),sleep(休眠指定时间)。基于系统内部的定时器,来实现的。

2.3.1 标准库中的定时器

  • 标准库中提供了一个Timer类,Timer类的核心方法为schedule
  • 在schedule包含了两个参数,第一个参数指定了即将要执行的任务代码,第二个参数指定了多长时间之后执行
public class TestDome5 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello timer");
            }
        }, 3000);

        System.out.println("开始运行:");  //开始运行3秒后,打印hello timer。
        
    }
}

2.3.2 实现定时器

1)描述任务

创建一个专门的类来表示任务:

//创建一个类,表示一个任务。
class MyTask{
    //用Runnable来表示具体要干什么
    private Runnable runnable;
    //任务具体什么时候干,保存任务要延迟的时间。
    private long delay;

    //构造方法
    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.delay = delay;
    }

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

2)组织任务

使用一定的数据结构把一些任务给放到一起,通过一定的数据结构来组织。

安排任务的时候,这些任务的顺序是无序的,但是执行任务的时候,这就不是无序的了!!!,这个要按照时间先后来执行。这就需要一个能排序的数据结构,而堆这个数据结构就是比较适合的。使用优先级队列。

class MyTimer{
    //因为此处可能在多个线程里进行注册任务,同时还有一个专门的线程来取任务执行,这里就要注意线程安全了
    PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable, long delay){
        MyTask myTask = new MyTask(runnable, delay);
        queue.put(myTask);
    }
}

3)时间到了执行任务

需要先执行时间最靠前的任务,就需要就一个线程,去判断当前优先级队列的队首元素,看看这个最靠前的任务是不是到执行时间了

class MyTimer{
    //因为此处可能在多个线程里进行注册任务,同时还有一个专门的线程来取任务执行,这里就要注意线程安全了
    PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable, long delay){
        MyTask myTask = new MyTask(runnable, delay);
        queue.put(myTask);
    }

    public MyTimer(){
        //创建一个线程
        Thread thread = new Thread(()->{
            while(true){
                try {
                    //取出最早执行的一个任务
                    MyTask myTask = queue.take();
                    //取出当前时间
                    long currentTime = System.currentTimeMillis();
                    //判断是否要执行这个任务
                    if(myTask.getTime() > currentTime){
                        //不执行
                        queue.put(myTask);
                    }else{
                        //执行
                        myTask.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        thread.start();
    }
}

4)上面代码第一个问题:没有指定比较规则

我们使用这个优先级阻塞队列的时候,其中的比较规则并不是默认就存在的,这个需要我们手动指定,我们所写的这个代码是按照时间的大小来比较。

所以需要我们加入Comparable接口,和实现比较方法。

class MyTask implements Comparable<MyTask>{
    @Override
    public int compareTo(MyTask o) {
        return (int)(this.time - o.time);
    }
}

5)上面代码第二个问题:忙等

在这个代码的线程中 while (true) 转的太快了,造成了无意义的 CPU 浪费。
如果队列中的任务是空着的,还好,这个线程就进入了阻塞状态,但是如果队列中的任务不空,并且任务的时间还没有到,那么这个while循环就会一直的转动,一直比较当前的时间到没有到。说是等待,但是一直占着CPU的资源,没有真正的等待。

引入一个 locker对象,借助该对象的 wait / notify 来解决 while (true) 的忙等问题。

为什么我们用wait,而不用sleep呢?
因为sleep不能被中途唤醒,wait能够被中途唤醒。
我们在等待的过程中,有可能插入新的任务,新的任务有可能比所有的任务都要早,所以就需要在schedule操作中,加上一个notify操作,唤醒线程中的wait,确保新的任务即使是最早的,也能先执行新任务。

class MyTimer{
    //因为此处可能在多个线程里进行注册任务,同时还有一个专门的线程来取任务执行,这里就要注意线程安全了
    PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable, long delay){
        MyTask myTask = new MyTask(runnable, delay);
        queue.put(myTask);
        synchronized (locker){
            locker.notify();  //每添加一个任务唤醒一次线程。
        }
    }

    Object locker = new Object();

    public MyTimer(){
        //创建一个线程
        Thread thread = new Thread(()->{
            while(true){
                try {
                    //取出最早执行的一个任务
                    MyTask myTask = queue.take();
                    //取出当前时间
                    long currentTime = System.currentTimeMillis();
                    //判断是否要执行这个任务
                    if(myTask.getTime() > currentTime){
                        //不执行
                        queue.put(myTask);
                        //为了避免忙等要加上wait。要想使用wait就需要使用synchronized
                        synchronized (locker){
                            locker.wait(myTask.getTime() - currentTime);
                        }
                    }else{
                        //执行
                        myTask.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        thread.start();
    }
}

2.3.3 完整代码

import java.util.concurrent.PriorityBlockingQueue;
//创建一个类,表示一个任务。
class MyTask implements Comparable<MyTask>{
    //用Runnable来表示具体要干什么
    private Runnable runnable;
    //任务具体什么时候干,保存任务要延迟的时间。
    private long time;

    //构造方法
    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

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

    public long getTime() {
        return time;
    }

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

class MyTimer{
    //因为此处可能在多个线程里进行注册任务,同时还有一个专门的线程来取任务执行,这里就要注意线程安全了
    PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable, long delay){
        MyTask myTask = new MyTask(runnable, delay);
        queue.put(myTask);
        synchronized (locker){
            locker.notify();  //每添加一个任务唤醒一次线程。
        }
    }

    Object locker = new Object();

    public MyTimer(){
        //创建一个线程
        Thread thread = new Thread(()->{
            while(true){
                try {
                    //取出最早执行的一个任务
                    MyTask myTask = queue.take();
                    //取出当前时间
                    long currentTime = System.currentTimeMillis();
                    //判断是否要执行这个任务
                    if(myTask.getTime() > currentTime){
                        //不执行
                        queue.put(myTask);
                        //为了避免忙等要加上wait。要想使用wait就需要使用synchronized
                        synchronized (locker){
                            locker.wait(myTask.getTime() - currentTime);
                        }
                    }else{
                        //执行
                        myTask.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        thread.start();
    }

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

        System.out.println("开始运行:");

    }
}

2.4 线程池

进程比较重,频繁的创建和销毁线程的开销大。
解决方案:进程池 或者 线程

线程虽然比进程更轻量,但是如果创建销毁的频率进一步增加,仍然会发现开销还是会有的。
解决方案:线程池 或者 协程

把线程提前创建好,放到线程池里面,当我们后面需要的时候,直接从线程池里面取出,就不必从系统申请了。线程用完了,也不需要销毁,而是直接放回到线程池里面。

2.4.1 线程放到线程池中,为什么申请释放更快了

我们首先了解一些系统底层的架构。
详细讲解 —— 多线程的四个案例、单例模式、阻塞队列、定时器、线程池(Java EE初阶)(万字长文)_第8张图片
系统的底层架构是大致可以分为上面5个部分。

==我们自己的写的代码在应用程序这一层,这里的代码称为 “用户态” 运行的代码。 ==

有一些代码,需要调用操作系统的API,进一步的逻辑就会在内核中执行。
例如:调用一个System.out.println,本质上是要经过write系统调用,进入内核中,内核执行一段逻辑,控制显示器输出字符串。

其中在内核中运行的代码,被称为 “内核态” 运行的代码。

而我们创建线程就是需要进入内核(创建线程就是在内核中申请一个PCB,然后加入到链表中),其中Thread.start就是在内核中执行。

我们把创建好的线程放到一个池子中,由于池子就是使用“用户态”代码实现的,所以从池子中取出和存放是不需要涉及到“内核态”的。

我们认为纯用户态的代码比经过内核态的代码效率要更高。

2.4.2 Java标准库中的线程池

标准库中的线程池,叫做:ThreadPoolExecutor

ThreadPoolExecutor的构造方法。

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, 
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, 
RejectedExecutionHandler handler) 
创建一个新 ThreadPoolExecutor给定的初始参数。 
  • int corePoolSize —— 核心线程数
  • int maximumPoolSize —— 最大线程数
  • long keepAliveTime —— 允许不是核心线程的最大空空闲时间
  • TimeUnit unit —— 时间单位
  • BlockingQueue workQueue —— 任务队列,线程池中会提供一个submit方法,让程序员把任务注册到线程池中,加到任务队列中
  • ThreadFactory threadFactory —— 线程工厂,线程是怎么创建出来的
  • RejectedExecutionHandler handler —— 拒绝策略,当任务队列满了,怎么做(直接忽略最新的任务,阻塞等待,直接丢弃最老的任务)。

虽然线程池里面的参数这么多,但是最终要的参数,还是核心线程数和最大线程数。那么怎么确定线程池中线程的数量呢?

如何确定线程池中线程的数量

通过性能测试的方式,找到合适的值。

例如:
写一个服务器程序,服务器里通过线程池,多线程的处理用户请求,就可以对这个服务器进行性能测试。
根据不同的业务场景,构造一个合适的请求数量。
然后让不同的线程池的线程数,来观察,程序处理服务器的速度和CPU的占用率。线程多了,CPU占用的就多了,如果线程变少了,CPU的占用也就变少了。
需要找到一个线程数量和CPU占用的平衡点。

一般CPU不能占用满的,要留有一定的CPU资源,来处理一些突发的情况。

标准库中简化版的线程池

上面ThreadPoolExecutor这个线程池实现起来太过于麻烦,在标准库中还提供一个简化的版本(Executors)。

Executors 创建线程池的几种方式:

  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池
  • newSingleThreadExecutor: 创建只包含单个线程的线程池
  • newScheduledThreadPool: 设定延迟时间后执行命令
public class TestDome7 {
    public static void main(String[] args) {
        //使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池。
        //返回值类型为 ExecutorService。
        ExecutorService pool = Executors.newFixedThreadPool(10);
        //通过 ExecutorService.submit 可以注册一个任务到线程池。
        pool.submit(new Runnable() {
            @Override
            public void run() {
                for(int i=0; i<100; i++){
                    System.out.println("hello pool");
                }
            }
        });
    }
}

2.4.3 实现线程池

  1. 先能够描述任务(直接使用Runnable)
  2. 需要组织任务(直接使用BlockingQueue)
  3. 能够描述工作的线程
  4. 还需要组织这些线程
  5. 需要实现,向线程池中添加任务
class MyThreadPool{
    //1. 描述任务使用Runnable
    //2. 组织任务使用BlockingQueue
    //使用堵塞队列用来存放要执行的任务(Runnable)。
    private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();

    //3. 描述工作的线程
    //描述工作(Worker)的内部类
    static class Worker extends Thread{
        private BlockingQueue<Runnable> queue = null;
        //重写构造方法,为了获得阻塞队列queue
        public Worker(BlockingQueue<Runnable> queue){
            this.queue = queue;
        }

        //在Worker类中重写Thread类的run方法
        @Override
        public void run() {
            while(true){
                try {
                    //从阻塞队列中取出任务(Runnable)
                    Runnable runnable = queue.take();
                    //执行任务(Runnable),Runnable这个接口的run方法就是我们要存放的任务
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //4. 组织这些线程
    //用数组来存放其中创建的线程(Worker)
    private List<Thread> workers = new ArrayList<>();

    //在构造方法中创建出需要的线程
    public MyThreadPool(int number){
        for(int i=0; i<number; i++){
            //Worker(queue),为了将MyThreadPool的类中的阻塞队列,传到Worker类中。
            Worker worker = new Worker(queue);
            //执行start就是执行了run方法,而且还创建了线程
            worker.start();
            workers.add(worker);
        }
    }

    //5. 从外界添加任务
    //如果要添加任务就使用submit这个函数
    public void submit(Runnable runnable){
        try {
            //将任务(Runnable)添加到阻塞队列中去
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//测试代码
public class TestDome8 {
    public static void main(String[] args) {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        myThreadPool.submit(new Runnable() {
            @Override
            public void run() {
                for(int i=0; i<100; i++){
                    System.out.println("hello thread");
                }
            }
        });
    }
}

你可能感兴趣的:(Java,EE初阶,java-ee,java,开发语言,单例模式,后端)