Java线程③-多线程案例

一:单例模式

🔺注:单例模式是校招中最常考的设计模式之一


▲设计模式:设计模式好比象棋中的 "棋谱"。红方当头炮,黑方马来跳。针对红方的一些走法,,黑方应招的时候有 一些固定的套路。 按照套路来走局势就不会吃亏

软件开发中也有很多常见的 "问题场景"。针对这些问题场景, 大佬们总结出了一些固定的套路。按照 这个套路来实现代码, 也不会吃亏


💗单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例

💜简单理解就是只能被new一次


单例模式具体的实现方式, 分成 "饿汉模式""懒汉模式" 两种

重点在于懒汉模式!!!

(1)饿汉模式

①特点

⧎类加载的同时, 创建实例

⯭只能被new一次


💚一般称类为Singleton

②代码
// 单例模式(饿汉模式)
class Singleton {
    //类加载的同时, 创建实例
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {    //创造一个静态方法getInstance,作用是返回对象
        return instance;
    }

    // 做出一个限制,将构造方法封装加上private,禁止别人去 new 一个实例!
    private Singleton() {}
}

public class Demo19 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        // Singleton s3 = new Singleton();    //这条代码会报错;上面已经将构造方法封装加上private了

        System.out.println(s1 == s2);  //true
    }
}
③关于线程安全问题

🌟饿汉模式并不会出现线程安全问题!!!


💖原因:

 这个getInstance静态方法的作用就是返回instance,本质操作就是在读取变量

从上一篇文章Java线程②中我们可以得知多个线程读取同一个变量是可行的

换句话说,多线程读取同一变量不会引发线程安全问题!!!

(2)懒汉模式-普通版

①特点

⧎类加载的时候不创建实例. 第一次使用的时候才创建实例(能不创建就不创建)

⯭只能被new一次


💚一般称类为SingletonLazy

②代码
class SingletonLazy {
    //类加载的时候不创建实例. 第一次使用的时候才创建实例.
    private static SingletonLazy instance = null;    //类加载时设置为空

    //创造一个静态方法getInstance,作用是判断如果是第一次使用就创建实例
    public static SingletonLazy getInstance() {  
        if (instance==null){    //如果为null就是没有实例化过就进去实例化
            instance = new SingletonLazy();
        }
        return instance;   //不为null也就是实例化了,返回即可
    }

    // 做出一个限制,将构造方法封装加上private,禁止别人去 new 一个实例!
    private SingletonLazy() {}
}

public class Demo20 {
    public static void main(String[] args) {
        //s1调用时,因为是第一次使用instance为null,所以进入if创建实例
        SingletonLazy s1 = SingletonLazy.getInstance();

        //s2调用时,因为已经创建过实例了,此时instance不为null,所以直接返回即可
        SingletonLazy s2 = SingletonLazy.getInstance();

        System.out.println(s1==s2);

        // SingletonLazy s3 = new SingletonLazy();    //这条代码会报错;上面已经将构造方法封装加上private了
    }
}
③关于线程安全问题-普通版

🌟懒汉模式-普通版是会出现线程安全问题!!!




⭐原因:与原子性、内存可见性以及指令重排序有关

instance=new SingletonLazy();这条代码是搭配if条件语句来使用的,分成了两部分代码,就不是原子的,有可能会出现线程安全问题!

假设我们现在有t1、t2两个线程去调用getInstance,t1先判断,t2没等t1实例化就判断了,此时t2中就认为instance是null,然后t1实例化,t2中instance为null,就又实例化一次,最终我们就能看到实例化new了两次,此时就会出现线程安全问题!

(3)懒汉模式-加锁版

①特点

⧎类加载的时候不创建实例. 第一次使用的时候才创建实例

⯭只能被new一次

⚝用synchronized给方法加锁


💚一般称类为SingletonLazy

②代码
class SingletonLazy {
    //类加载的时候不创建实例. 第一次使用的时候才创建实例.
    private static SingletonLazy instance = null;    //类加载时设置为空

    //创造一个静态方法getInstance,作用是判断如果是第一次使用就创建实例
    public static SingletonLazy getInstance() {    
        //synchronized修饰静态代码块(上一篇文章Java线程②有提及)
        //给整个if条件加锁,让整段代码作为一个原子不可分割
        synchronized (SingletonLazy.class) {
            if (instance == null) {    //如果为null就是没有实例化过就进去实例化
                instance = new SingletonLazy();
            }
        }
        return instance;   //不为null也就是实例化了,返回即可
    }

    // 做出一个限制,将构造方法封装加上private,禁止别人去 new 一个实例!
    private SingletonLazy() {}
}

public class Demo21 {
    public static void main(String[] args) {
        //s1调用时,因为是第一次使用instance为null,所以进入if创建实例
        SingletonLazy s1 = SingletonLazy.getInstance();

        //s2调用时,因为已经创建过实例了,此时instance不为null,所以直接返回即可
        SingletonLazy s2 = SingletonLazy.getInstance();

        System.out.println(s1==s2);

        // SingletonLazy s3 = new SingletonLazy();    //这条代码会报错;上面已经将构造方法封装加上private了
    }
}

 ③关于线程安全问题-加锁版

🌟懒汉模式-加锁版仍然还会出现线程安全问题!!!

🔯虽然解决了原子性,但是还会出现线程安全问题,除此之外还会出现效率低下问题!!!


1.无脑加锁导致效率低下

原因:使用了synchronized就代表了以后每次调用getInstance都会加锁,但是这种加锁是不必要的;首次调用加锁,很正常,可第二次也加锁,就没啥必要了,会降低效率,因为线程不安全原因主要是首次new对象的时候才存在问题,此时t2可能在t1没实例化之前就判断了,如果把对象new好了,后续再调用getInstance就都是return读操作而已!!!

(双层if解决)


2.内存可见性以及指令重排序导致线程安全问题

原因:t1线程在修改为instance的值不为null后,就结束代码块然后释放锁,t2就能得到锁然后从阻塞中恢复,如果编译器进行优化,那么t2就一定能够读取到t1修改过的值吗?这里就会出现内存可见性问题!!!

(关键字volatile解决)

(4)懒汉模式-双层if加锁版

①特点

⧎类加载的时候不创建实例. 第一次使用的时候才创建实例

⯭只能被new一次

⚝用synchronized给方法加锁,且在synchronized外再加一层if判断


💚外层先判断是否要加锁,内层再判断是否要真正加锁进行实例化

②代码
class SingletonLazy {
    //类加载的时候不创建实例. 第一次使用的时候才创建实例.
    private static SingletonLazy instance = null;    //类加载时设置为空
    
    public static SingletonLazy getInstance() {
        //外层if进行判断,如果instance == null则是首次调用需要加锁,如果是非null就说明是后续调用,从而不再进行加锁
        if(instance == null){
            synchronized (SingletonLazy.class) {
                if (instance == null) {    //如果为null就是没有实例化过就进去实例化
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;   //不为null也就是实例化了,返回即可
    }

    // 做出一个限制,将构造方法封装加上private,禁止别人去 new 一个实例!
    private SingletonLazy() {}
}

public class Demo22 {
    public static void main(String[] args) {
        //s1调用时,因为是第一次使用instance为null,所以进入if创建实例
        SingletonLazy s1 = SingletonLazy.getInstance();

        //s2调用时,因为已经创建过实例了,此时instance不为null,所以直接返回即可
        SingletonLazy s2 = SingletonLazy.getInstance();

        System.out.println(s1==s2);

        // SingletonLazy s3 = new SingletonLazy();    //这条代码会报错;上面已经将构造方法封装加上private了
    }
}

 ③关于线程安全问题-双层if加锁版

🌟懒汉模式-双层if加锁版仍然还会出现线程安全问题!!!


原因:内存可见性以及指令重排序导致线程安全问题

内存可见性上面已经说过了,这里说说指令重排序的问题


指令重排序:我们知道指令重排序也是编译器优化带来的“好心办坏事”;在保证原有逻辑执行不变的前提下,对代码执行顺序进行调整,使调整之后执行效率提高;但是对于多线程来说,调整执行顺序可能会导致线程安全问题!!!


这段代码其实是分三步的

①给对象创造出内存空间,得到内存地址

②在空间上调用构造方法,给对象进行初始化

③把内存地址赋值给instance引用变量


✹如果在多线程中,这几个顺序如果发生变化就会导致线程安全问题

例:假设由于指令重排序,编译器的优化,令这条代码是按照①③②顺序执行,第一个线程的操作就是先给对象创建内存空间,把地址给instance,此时已经执行了①③;但是如果这时发生了线程转换,转给了第二个线程,可是我的第一个线程还没来得及初始化呢,那么第二个线程在进行判断if(instance == null)的时候,就会因为前面第一个线程已经创建出实例而判断不为空,于是直接返回第一个线程创建的对象instance,并且后续可能用到instance的方法和属性。那么问题来了,此处第二个线程拿到的是一个不完整的对象,因为第一个线程还没有初始化。拿到一个不完整的对象,后续出现什么问题很难保证,作为程序猿我们也不想发生这种事情吧??!!

(用关键字volatile解决,此时的代码就必须按指定顺序进行执行)

(5)懒汉模式-正确版

①特点

⧎类加载的时候不创建实例. 第一次使用的时候才创建实例

⯭只能被new一次

⚝用synchronized给方法加锁,且在synchronized外再加一层if判断

❄给instance加上volatile


💚外层先判断是否要加锁,内层再判断是否要真正加锁进行实例化

💚给instance加上volatile彻底解决所有问题

②代码
class SingletonLazy {
    //类加载的时候不创建实例. 第一次使用的时候才创建实例
    //给instance加上volatile
    private static volatile SingletonLazy instance = null;    //类加载时设置为空

    public static SingletonLazy getInstance() {
        //外层if进行判断,如果instance == null则是首次调用需要加锁,如果是非null就说明是后续调用,从而不再进行加锁
        if(instance == null){
            synchronized (SingletonLazy.class) {
                if (instance == null) {    //如果为null就是没有实例化过就进去实例化
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;   //不为null也就是实例化了,返回即可
    }

    // 做出一个限制,将构造方法封装加上private,禁止别人去 new 一个实例!
    private SingletonLazy() {}
}

public class Demo22 {
    public static void main(String[] args) {
        //s1调用时,因为是第一次使用instance为null,所以进入if创建实例
        SingletonLazy s1 = SingletonLazy.getInstance();

        //s2调用时,因为已经创建过实例了,此时instance不为null,所以直接返回即可
        SingletonLazy s2 = SingletonLazy.getInstance();

        System.out.println(s1==s2);

        // SingletonLazy s3 = new SingletonLazy();    //这条代码会报错;上面已经将构造方法封装加上private了
    }
}

  ③关于线程安全问题-正确版

同时拥有synchronized、双层if、volatile的懒汉模式才是正确的!!!


但是给面试官写的时候,不要一开始就写正确版本的,可以先写普通版本➔加锁版➔双层if加锁版➔正确版,主打的就是一个欲擒故纵,不但让面试官有台阶下,你也能给他一个很好的印象!!!

二:阻塞式队列

(1)概念

阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则

(2)特点

阻塞队列能是一种线程安全的数据结构


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

当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素

(3)应用场景

生产者消费者模型

(4)生产者消费者模型

1.概念

一种非常典型的开发模型,也是处理多线程问题的方式!!!

2.优势

①解耦合

💗降低模块之间的耦合,也就是降低模块之间的影响 

比如过年一家人一起包饺子

一般都是有明确分工,比如一个人负责擀饺子皮,其他人负责包

擀饺子皮的人就是 "生产者",包饺子的人就是 "消费者"

擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包)

包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超市买的)


例如A是直接把请求发给B,由B去完成一系列操作,此时A和B之间的耦合就比较明显!数据传输是双向的,假设B挂了就可能对A造成很大影响,会造成A给B发送请求那个接口很容易抛出异常,且异常要是没处理好A也就跟着挂了;反之,B要给A返回响应数据(比如充值是否成功,现余额是多少等等),假设A挂了就可能对B造成很大影响,会造成B给A返回响应那个接口抛出异常!如果要新增一个服务器C,此时A服务器的代码要做出很大的修改!!!


引入生产者消费者模型和阻塞队列就能有效解决上述问题:

缺点:效率降低;因为之前是A直接通信给B的,是一次通信!而现在A要先发送给队列,再从队列发给B,这就是两次通信!

此时A与B之间不是直接通信,而是都把请求写进一个阻塞队列中,通过阻塞队列,A和B就能很好的解耦合了。假设A或者B挂了,由于他们彼此之间不会直接交互,没有啥太大影响,不会出现一个挂了把另外一个也顺着挂了。比如A挂了,B并不是直接跟A交互,而是跟队列交互,B根本就不知道A的存在,而队列又工作的好好的,不会影响到B!如果要新增一个服务器C,此时A服务器不需要任何修改,只要让C从队列中取元素即可!!!甚至A都不知道C存在,你乐意有个DEFG都行,爱咋咋地!!!

②削峰填谷

💗平衡了生产者和消费者的处理能力,防止服务器被突然到来的一波请求直接冲垮


服务器收到客户端/用户的请求不是一成不变的,可能会因为一些突发事件,导致请求数暴增

一台服务器的同一时刻能处理的请求是有上限的,且不同服务器的处理请求上限不一样

是因为机器的硬件资源是有限的,服务器每处理一次请求,都需要消耗一定的硬件资源


例如


引入生产者消费者模型和阻塞队列就能有效解决上述问题:

(5)标准库中的阻塞队列

💚在 Java 标准库中内置了阻塞队列,可直接使用标准库里的


①BlockingQueue 是一个接口,真正实现的类是 LinkedBlockingQueue


②put 方法用于阻塞式的入队列,take 用于阻塞式的出队列

💛出队列,如果没有 put 或者队列为空直接 take,就会阻塞


③BlockingQueue 也有 offer, poll, peek 等方法,但是这些方法不带有阻塞特性


④三种实现方式

 


import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

//使用一下Java 标准库中内置的阻塞队列
public class Demo23 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);   //代表最大容量是10个元素

        //put方法入队列
        queue.put("hlizoo");

        //take方法出队列
        String elem = queue.take();

        System.out.println(elem);

        //上面已经取出过元素了,此时队列为空,如果这里再次取一次,会阻塞!!!
        elem = queue.take();

    }
}

(6)生产者消费者模型代码

// 生产者消费者模型
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class Demo24 {
    public static void main(String[] args) {
        // 搞一个阻塞队列, 作为交易场所
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();


        // t1负责生产元素
        Thread t1 = new Thread(() -> {
            int count = 0;
            while (true) {
                try {
                    queue.put(count);     //放入元素
                    System.out.println("生产元素: " + count);
                    count++;

                    Thread.sleep(1000);     //限制节奏;每次入队之后隔1s再入队
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });


        // t2负责消费元素
        Thread t2 = new Thread(() -> {
            while (true) {
                try {
                    Integer n = queue.take();   //取出元素
                    System.out.println("消费元素: " + n);      //虽然没有sleep,但是节奏跟着生产者t1走
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });


        t1.start();   //启动t1线程
        t2.start();   //启动t2线程

    }
}

(7)模拟实现阻塞队列

💛通过 "循环队列" 的方式来实现


💜使用 synchronized 进行加锁控制

💜使用 volatile保证内存可见性和指令重排序


💓put 插入元素的时候, 判定如果队列满了, 就进行 wait

(注意, 要在循环中进行 wait)

原因:如果用了if,假设put操作因为队列满了而进入wait阻塞,过了一会wait被唤醒了,那么当被唤醒的时候此时的队列就一定不满了吗?是否还存在着队列仍然满的可能性?万一出现了队列还是满的,此时就意味着如果接下来的代码继续执行,就可能把之前的元素给覆盖了;当用了while循环,wait被唤醒后就继续判断条件嘛,如果条件还是size >= items.length队列满了,那就继续wait,否则,就跳出循环往下执行


💓take 取出元素的时候, 判定如果队列为空, 就进行 wait

(注意, 要在循环中进行 wait)

原因同上

class MyBlockingQueue {
    //队列是 队尾进 队头出,满足先进先出原则!!!
    // 使用一个 String 类型的数组items来保存元素. 假设这里只存 String
    private String[] items = new String[1000];

    // 当 head 和 tail 相等(重合), 相当于空的队列
    // 指向队列的头部
    volatile int head = 0;

    // 指向队列的尾部的下一个元素. 总的来说, 队列中有效元素的范围 [head, tail)
    volatile private int tail = 0;

    // 使用 size 来表示元素个数.(用来表示当size=items.length时队列已满)
    volatile private int size = 0;

    //定义一个锁对象
    private Object locker = new Object();

    // 入队列
    public void put(String elem) throws InterruptedException {
        // 此处的写法就相当于直接把 synchronized 写到方法上了
        synchronized (locker) {
            while (size >= items.length) {
                //数组元素个数大于等于数组长度就表示元素已满
                // 队列满了,阻塞等待!
                locker.wait();
            }
            //元素没满就添加元素入队列
            items[tail] = elem;
            //元素添加完后tail记得往后走
            tail++;
            //判断tail如果走到了末尾就回到0下标
            if (tail >= items.length) {
                tail = 0;
            }
            //添加,元素个数加1
            size++;
            // 用来唤醒队列为空的阻塞情况
            locker.notify();
        }
    }

    // 出队列
    public String take() throws InterruptedException {
        synchronized (locker) {
            while (size == 0) {
                //数组元素个数为0就代表队列为空
                // 队列为空, 暂时不能出队列,阻塞等待!
                locker.wait();
            }
            //队列不为空就取出元素放elem
            String elem = items[head];
            //取完元素记得head往后走
            head++;
            //判断head如果走到了末尾就回到0下标
            if (head >= items.length) {
                head = 0;
            }
            //取出,元素个数减1
            size--;
            // 使用这个 notify 来唤醒队列满的阻塞情况
            locker.notify();
            return elem;
        }
    }
}

public class Demo25 {
    public static void main(String[] args) throws InterruptedException {
        // 创建两个线程, 表示生产者和消费者
        MyBlockingQueue queue = new MyBlockingQueue();


        Thread t1 = new Thread(() -> {
            int count = 0;
            while (true) {
                try {
                    queue.put(count + "");
                    System.out.println("生产元素: " + count);
                    count++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });


        Thread t2 = new Thread(() -> {
            while (true) {
                try {
                    String count = queue.take();
                    System.out.println("消费元素: " + count);

                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });


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

三:定时器

(1)概念

定时器也是软件开发中的一个重要组件,类似于一个 "闹钟"!

达到一个设定的时间之后,就执行某个指定好的代码!

(2)标准库中的定时器

Timer timer = new Timer();         //Timer类的实例化
timer.schedule(new TimerTask() {   //timer中的具体方法schedule(与runnable类似用法)
    @Override
    public void run() {
        //具体要干什么
    }
},x);  //x ms秒后执行

标准库中提供了一个 Timer 类

💘Timer 类的核心方法为 schedule


schedule方法中包含两个参数

💗第一个参数new TimerTask()指定即将要执行的任务代码

💗第二个参数指定多长时间之后执行 (单位为毫秒)


💚new TimerTask()里面重写的run方法就表示定时器到指定时间后要干什么


💕 TimerTask()与runnable()类似,主要原因是实现了Runnable接口


💕 TimerTask()实现了Runnable接口,run方法也就是到点了需要执行什么代码

①代码

import java.util.Timer;
import java.util.TimerTask;

//定时器的使用
public class Demo26 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        
        //定义三个定时器

        timer.schedule(new TimerTask() {
            @Override
            public void run() {    //定时器需要干什么事
                System.out.println("Hello MyAlarm3!!!");
            }
        },3000);   //3s之后开始

        timer.schedule(new TimerTask() {
            @Override
            public void run() {    //定时器需要干什么事
                System.out.println("Hello MyAlarm2!!!");
            }
        },2000);   //2s之后开始

        timer.schedule(new TimerTask() {
            @Override
            public void run() {    //定时器需要干什么事
                System.out.println("Hello MyAlarm1!!!");
            }
        },1000);   //1s之后开始
    }
}

②执行效果

(3)模拟实现定时器

①创建一个MyTimerTask类, 用来描述定时器中的某一个任务,这个任务包括两个方面,一个是任务的内容一个是任务的实际执行时间

✹因为优先级队列,所以需要实现 Comparable 接口


②定义一个类叫MyTimer作为定时器类的本体

用优先级队列把多个任务(MyTimerTask)组织起来

◬原因:使用优先级队列在于我们只需要盯住时间最靠前的任务即可,最早执行的任务时间还没到,则其他任务时间肯定没到,而优先级队列的队首元素就是时间最小的


③再在MyTimer类中创建一个构造方法,里面创建一个线程作为 "扫描线程", 一方面去负责监控队首元素是否到点了, 是否应该执行; 一方面当任务到点之后,用run方法去执行任务

import java.util.PriorityQueue;


// 创建一个MyTimerTask类, 用来描述定时器中的某一个任务!!!
class MyTimerTask implements Comparable<MyTimerTask> {
    // 任务实际具体啥时候执行. 毫秒级的时间戳
    private long time;

    // 任务具体是啥,根据Runnable中的run方法去实现
    private Runnable runnable;

    public MyTimerTask(Runnable runnable, long delay) {
        // delay 是一个相对的时间差,表示多久之后执行任务
        // time作为实际执行时间,time=系统时间+delay
        time = System.currentTimeMillis() + delay;
        //runnable表示具体要干什么
        this.runnable = runnable;
    }

    //获取一下执行时间
    public long getTime() {
        return time;
    }

    //获取一下runnable再调用run方法实现具体任务
    public Runnable getRunnable() {
        return runnable;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        // 认为时间小的, 优先级高
        // 最终时间最小的元素, 就会放到队首.
        return (int) (this.time - o.time);
        // return (int) (o.time - this.time);   //表示时间大的优先级高
    }
}






//创建一个MyTimer类,作为定时器类的本体,把多个任务<MyTimerTask>组织起来!!!
class MyTimer {
    // 使用优先级队列, 来保存上述<MyTimerTask>的 N 个任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    // 用来加锁的对象locker
    private Object locker = new Object();

    // 定时器的核心方法, 就是把要执行的新的任务添加到队列中.
    public void schedule(Runnable runnable, long delay) {
        synchronized (locker) {
            //初始化任务task
            MyTimerTask task = new MyTimerTask(runnable, delay);
            //将task任务添加到队列(元素)
            queue.offer(task);
            // 每次来新的任务, 都唤醒一下之前的扫描线程
            // 好让扫描线程根据最新的任务情况, 重新规划等待时间.
            locker.notify();
        }
    }

    // MyTimer 中还需要构造一个 "扫描线程", 一方面去负责监控队首元素是否到点了, 是否应该执行; 一方面当任务到点之后,
    // 就要调用这里的 Runnable 的 Run 方法来完成任务
    public MyTimer() {
        // 扫描线程
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            // 注意, 当前如果队列为空, 此时就不应该去取这里的元素.
                            // 此处使用 wait 等待更合适. 如果使用 continue, 就会使这个线程 while 循环运行的飞快,
                            // 也会陷入一个高频占用 cpu 的状态(忙等).
                            locker.wait();
                        }

                        //task获取一下优先级最高的元素,也就是最先执行的任务
                        MyTimerTask task = queue.peek();
                        //获取当前时间
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            // 假设当前时间是 14:01, 任务时间是 14:00, 此时就意味着应该要执行这个任务了.
                            //上述task已经获得最先执行的任务,后面马上就要执行了
                            //执行完任务这个任务肯定就要删除了,这里就移除优先级最高也就是已完成的元素
                            queue.poll();
                            //调用run方法开始执行任务
                            task.getRunnable().run();
                        } else {
                            // 让当前扫描线程休眠一下, 按照时间差来进行休眠,看看离任务执行还差多久.
                            // Thread.sleep(task.getTime() - curTime);
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

四:线程池

(1)概念和作用

概念:

将一些要释放的资源,不要着急释放,而是先放到一个“池子”里,以备后续使用

申请资源的时候,也先提前把申请的资源申请好,也放到一个“池子”里,后续申请时也方便


作用:减少每次启动、销毁线程的损耗

问题:那么为什么从池里取线程就比从系统创建线程更加高效呢?


回答:

如果从系统这里创建线程,需要调用系统API,进一步的由操作系统内核去完成线程创建的过程,那么,这里就是内核给所有进程提供服务,这种由内核创建的过程是不可控的,因为它可能在创建的过程中会去执行别的事!

如果是从线程池里面获取线程,上述在内核中进行的操作就都提前做好了,取线程的过程中,纯粹靠的是用户代码完成即可,也即是纯用户态,这种是可控的,用户想立刻创建就用代码立刻创建!

(2)标准库中的线程池

①使用ExecutorService类


②然后Executors.静态方法创建线程池

Executors 本质上是 ThreadPoolExecutor 类的封装

💛以下都是属于Executors的静态方法

newFixedThreadPool()

newCachedThreadPool()

newSingleThreadExecutor()

newScheduledThreadPool()


③创建好线程池之后,使用submit方法就可将任务添加到线程池里

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

// 线程池
public class Demo28 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(4);  //创建出固定包含 4 个线程的线程池
        for (int i = 0; i < 10; i++) {
            service.submit(new Runnable() {     //调用submit方法添加任务
                @Override
                public void run() {    //在run方法里面执行
                    System.out.println("hello");   //任务就是打印hello
                }
            });
        }
    }
}

(3)ThreadPoolExecutor

💜ThreadPoolExecutor是一个接口更丰富的线程池类,提供了更多的可选参数

💙ThreadPoolExecutor里面的线程数会根据当前任务情况动态变化(自适应)

⭐从图里我们可以看到,ThreadPoolExecutor提供了七个可选参数


①int corePoolSize:核心线程数 

(表示最少得包含这些线程数目,哪怕线程池里没有任务)


②int maximumPoolSize:最大线程数

(最多不能超过这些线程数,哪怕你的线程池已经忙到冒烟了,也不能比这个数目多)


③long keepAliveTime:最大空闲时间

TimeUnit unit:时间单位

(当线程空闲超过了指定时间,就可以销毁了)


④BlockingQueue<Runnable> workQueue:线程池里的任务可以用阻塞队列来管理

线程池可以内置阻塞队列,也可以手动指定一个


⑤ThreadFactory threadFactory:工厂模式;通过这个工厂类创建线程


⑥RejectedExecutionHandler handler:拒绝方式/拒绝策略

(线程池里的阻塞队列,当阻塞队列满了之后,继续添加新任务时该如何应对?)

💓三种拒绝策略:

①直接抛出异常,线程池放弃工作,不管什么任务都不做,摆烂了

 

②哪个线程添加了这个新任务,这个线程就去执行这个任务

 

③放弃最早的任务,然后执行新的任务

④放弃最新的任务,继续执行之前的任务


(4)模拟实现线程池

使用一个 BlockingQueue 组织所有的任务


心操作为 submit, 将任务加入线程池中

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool {
    //实现一个阻塞式队列来管理线程池里的任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    // 通过submit方法, 来把任务添加到线程池中.
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    // n 表示线程池里有几个线程.
    // 创建了一个固定数量的线程池.
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                while (true) {
                    try {
                        // 取出任务, 并执行~~
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
}

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值