Java EE初阶 多线程案例


提示:以下是本篇文章正文内容,下面案例可供参考

一、单例模式

单例模式是一种设计模式,指的是写代码时有一些常见场景,设计模式就是针对这些常见场景给出的一些经典解决方案。

单例模式有两种典型实现:饿汉和懒汉

两者的区别在哪里呢?举个例子:
在这里插入图片描述
饿汉的单例模式,是比较着急去创建实例;
懒汉的单例模式,是不着急去创建实例,用的时候再创建。

1.1饿汉模式

//通过Singleton这个类来实现单例模式,保证Singleton这个类只有唯一实例
class Singleton{
    //1.使用static创建一个实例,并且立即实例化
    //这个instance对应的实例就是该类的唯一实例
    private static Singleton instance=new Singleton();
    //2.为了防止程序员在其他地方不小心new这个Singleton,就可以把构造方法设为private
    private Singleton(){}
    //3.提供一个方法,让外面能拿到唯一实例

    public static Singleton getInstance() {
        return instance;
    }

    //static修饰的成员-更准确说是“类成员”
    //不加static修饰的成员-更准确的说是“实例成员”
    //一个java程序中,一个类对象只存在一份(JVM保证)
    //进一步也就保证了类static成员也只有一份

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

在这里插入图片描述

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;
    }
}
public class Demo20 {
    public static void main(String[] args) {
        Singleton2 instance=Singleton2.getInstance();
    }
}

1.3懒汉模式-多线程版

上面的懒汉模式实现线程是不安全的
在这里插入图片描述
举例说明:
我们现在给一种可能的线程t1和t2的执行情况
在这里插入图片描述
第一步:t1进行load——我们把null加载到cpu(寄存器)上
在这里插入图片描述
第二步:t2进行load——我们把null也给加载到cpu上
在这里插入图片描述
第三步:t1进行cmp比较,也就是下图红色箭头所指,与null比较,发现相等
在这里插入图片描述
发现相等之后,执行save操作
在这里插入图片描述
t1执行完save之后,
到第四步——t2也进行cmp,也是与null比,发现相等,执行save操作
在这里插入图片描述
可以看到,当我们两个线程按上图所示顺序执行,我们的实例就不是我们想要的那样只创建一份了(图示创建了2次)。如果同时有n个线程执行的话,就可能创建n份了,显然是存在bug了。

那如何保证我们懒汉模式的线程安全呢?——加锁!

//实现单例模式-懒汉模式
    class Singleton2{
        //1.不立即初始化实例
    private static Singleton2 instance=null;
    //2.把构造方法设为private
    private Singleton2(){

    }
    //3.提供一个方法来获取到上述单例的实例
    //  只有当真正需要这个实例时,才创建
    public static Singleton2 getInstance(){
        synchronized (Singleton2.class){
            //使用类对象作为锁对象——类对象在一个程序中只有一个
            //这样就能保证调用getinstance的时候针对的是同一个对象加锁

            //我们加锁是要把多个不是原子的操作变成一个原子的操作,
            //所以我们synchronized放if外面
            //如果你synchronized放if里面,那么if的读和里面的修改仍然是两个分割的个体
            if(instance ==null){
                instance=new Singleton2();
            }
        }
        return instance;
    }
}

当前,虽然加锁后线程安全得到解决,但又有了新问题:

对于最初懒汉模式的代码说,线程不安全是发生在instance被初始化之前。未初始化的时候,多线程调用getinstance,就可能同时涉及到读和修改,但一旦instance被初始化之后(一定是null,if条件一定不成立了),getinstance操作只剩下两个读操作了——只剩读操作也就是一定线程安全了。

而按照上述的加锁方式,无论代码是初始化之后,还是初始化之前,每次调用getinstance都会进行加锁,也就意味着即使初始化之后(已经线程安全了),但是仍然存在大量锁竞争
在这里插入图片描述
ps:加锁确实能让代码保证线程安全,但也是牺牲了速度为代价

1.4懒汉模式-多线程版(改进)

那我们也有改进方案:让getinstance初始化之前,才进行加锁,初始化之后,就不再加锁了。在加锁这里再加上一层条件判断即可。

条件就是当前是否已经初始化完成 if(instance==null)

//实现单例模式-懒汉模式
    class Singleton2{
        //1.不立即初始化实例
    private static Singleton2 instance=null;
    //2.把构造方法设为private
    private Singleton2(){

    }
    //3.提供一个方法来获取到上述单例的实例
    //  只有当真正需要这个实例时,才创建
    public static Singleton2 getInstance(){
        if(instance==null){
            synchronized (Singleton2.class){
                //使用类对象作为锁对象——类对象在一个程序中只有一个
                //这样就能保证调用getinstance的时候针对的是同一个对象加锁

                //我们加锁是要把多个不是原子的操作变成一个原子的操作,
                //所以我们synchronized放if外面
                //如果你synchronized放if里面,那么if的读和里面的修改仍然是两个分割的个体
                if(instance ==null){
                    instance=new Singleton2();
                }
            }
        }
        return instance;
    }
}

在这里插入图片描述

ps1:这两个if虽然相邻,但实际上两个条件执行时机可能相差很大:加锁的时候可能出现代码阻塞,就会产生时间差。而在这个时间差中间,instance也是可能被其他线程给修改的。

ps2:这里还有一个问题——如果多个线程都去调用这个getinstance,就会造成大量的读instance操作,可能会让编译器把读内存操作优化成读寄存器操作。一旦触发了优化,后续如果一个线程已经完成了针对instance的修改,那么紧接着后面的线程都将感知不到这个修改,仍然把instance当成null(内存可见性,可见笔者上一篇多线程文章有详解)

内存可见性问题,可能会引起第一个if判断失效,但是对第二个if判断影响不大。(synchronize本身也能保证内存可见性),因此这样的内存可见性问题,只引起了第一层条件的误判,也就是导致不该加锁的加锁了,但是不会引起第二层if的误判(不至于创建多个实例)

而内存可见性问题解决办法也很简单,给instance加上volatile即可
在这里插入图片描述
注意!!!:饿汉模式和懒汉模式是非常经典的面试问题,面试一般就是现场写代码,大家一定要着重掌握

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
(图片来自比特就业课)

二、阻塞式队列

2.1阻塞队列是什么

阻塞队列同样是一个符合先进先出规则的队列,相比普通队列,阻塞队列又有一些其他方面的功能:
1.线程安全
2.产生阻塞效果
(1)如果队列为空,尝试出队列,就会出现阻塞,阻塞到队列不为空为止
(2)如果队列已满,尝试入队列,也会出现阻塞,阻塞到队列不为满为止

基于上述特性,就可以产生“生产者消费者模型”

2.2生产者消费者模型

生产者消费模型是我们日常开发中,处理多线程问题的一个典型方式。

我们举个例子:

我们过年包饺子,这个过程往往是需要多人分工,假设A,B,C三人来擀饺子皮,包饺子。。。

那我们现在有如下几种协作方式:
1.A、B、C分别每个人都是擀一个皮,然后包一个饺子;然后擀一个皮,包一个饺子。。。
存在一个问题:擀面杖只有一个(锁冲突比较激烈)

2.A专门擀饺子皮,B和C负责包饺子(常见情况-效率更高)

2这种就构成了——生产者消费模型:
A是饺子皮的生产者,要不断生产饺子皮;
B和C是饺子皮的消费者,要不断消耗饺子皮;
对于包饺子来说,用来放饺子皮的那个“盖帘”就是交易场所

而我们的阻塞队列就可以作为生产者消费模型中的交易场所

生产者消费模型,是实际开发中非常有用的一种多线程开发手段,尤其是在服务器开发的场景中。

举例说明:
假设,有两个服务器,A、B,A作为入口服务器直接接收用户的网络请求,B作为应用服务器,来给A提供一些数据。
在这里插入图片描述
如果不使用生产者消费模型,此时A和B的耦合性是比较强的!

耦合性比较强,也就是A和B联系性比较强。但是联系性比较强往往是坏事。

比如在开发A代码时,就得充分了解B提供的一些接口,
而开发B代码的时候也得充分了解到A是怎么调用的——这样会增加开发成本,
并且一旦我们想要把B服务器换成其他服务器,这样A的代码又需要较大改动
另外,一旦B挂了,A也会被连累挂掉

使用生产者消费者模型,就可以降低这里的耦合。
举例说明:

我们仍然是两个服务器A和B
在这里插入图片描述
但我们这里不再A直接调用B,而是在AB之间搞一个阻塞队列:
A给B发请求——A把请求送到阻塞队列里,然后B到阻塞队列里面取数据
在这里插入图片描述
B如果想返回结果——B把结果写回到队列里,A再从队列里取数据

对于请求来说:A是生产者,B是消费者
对于响应来说:A是消费者,B是生产者
而阻塞队列始终是交易场所

A只需要关注如何和阻塞队列交互,不需要认识B
B也只需要关注如何和阻塞队列交互,不需要认识A

A/B中的任意一个挂了,对于另一个影响几乎没有

综上生产者消费模型有如下优点
优点1:能够让多个服务器程序之间更充分的解耦合

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

削峰举例说明:

(1)不适用阻塞队列:
在这里插入图片描述
(2)使用阻塞队列
在这里插入图片描述
填谷举例说明:请求暴涨后它会有一个时间请求的大幅回落,而这个时候就可以把之前积压的请求由阻塞队列发给B服务器

削峰填谷:这样相对没有阻塞队列,就不会让B服务器太闲,也不会让它太忙

ps:实际开发中使用到的“阻塞队列”,并不仅仅是一个简单的数据结构了,而是一个/一组专门的服务器程序,并且它提供的功能也不仅仅是阻塞队列的功能,还会在这基础上提供更多的功能(对于数据持久化存储,支持多个数据通道、支持多节点容灾冗余备份、支持管理面板、方便配置参数。。。)

2.3标准库中的阻塞队列

package thread;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class Demo21 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue=new LinkedBlockingDeque<>();

        //队列实现可以基于数组,也可以基于链表,
        //这里除了LinkedBlockingDeque<>,也可以是ArrayBlockingQueue<>

        //入队列
        queue.put("hello");
        //这里会有一个异常抛一下:put操作可能会触发阻塞,阻塞需要去唤醒,唤醒可能就需要打断

        //出队列
        String s=queue.take();

        //阻塞队列也提供:
        // 入队列offer()
        // 出队列poll()
        //获取队头元素peek()等方法,
        // 但是常见还是用put和take,因为put和take是带阻塞的,上面三个不带阻塞
    }
}

2.4阻塞队列实现

我们自己实现阻塞队列也很简单:先实现一个普通队列,再加上线程安全和阻塞即可

而队列可以基于数组实现,也可以基于链表实现:
我们这里基于数组实现阻塞队列更简单,所以下面就直接写数组版本了(是一个循环队列)。

如下图:我们定义一个head指向队头,tail指向队尾。
其中,[head,tail)这样的前闭后开区间表示数组有效元素

入队列,就把新元素放到tail位置,并且tail++

在这里插入图片描述

比如我们这里插入一个1,tail++
在这里插入图片描述
再插入一个2,tail++
在这里插入图片描述
再插入一个3,tail++
在这里插入图片描述

以此类推,这样就构成了入队列操作。

那么出队列怎么办呢?
出队列,就把head位置的元素返回出去,并且head++
在这里插入图片描述

这里一个问题就是,tail一直++,然后加到数组尾部怎么办? 如下图:
在这里插入图片描述
这个时候tail想继续+,就得从头开始(循环队列)
在这里插入图片描述

另外,如何区分是空队列还是满队列呢?
我们之前说过,[head,tail)这样的前闭后开区间表示数组有效元素,那么如果head和tail重合了并且head位置,就是空的,如下图:
在这里插入图片描述
但是问题来了,如果整个数组都满了,head和tail也会重合,并且现在这种情况是不为空的。
在这里插入图片描述
对于上述情况,我们又下面的解决办法:
法(1)浪费一个格子:

head==tail认为空

在这里插入图片描述
head==tail+1认为是满
在这里插入图片描述
法(2)额外创建一个变量size,记录元素个数:
size=0,就是空
size=arr.length,就是满

代码如下:

class MyBlockingQueue{
    //保存数据的本体
    private int[] data=new int[1000];
    //有效元素个数
    private int size=0;
    //队首下标
    private int head=0;
    //队尾下标
    private int tail=0;

    //专门的锁对象
    private Object locker=new Object();

    //入队列
    public void put(int value) throws InterruptedException {
        synchronized (locker){
            if(size== data.length){
                locker.wait();//队列满了
                //哪个对象加锁,就针对哪个对象wait
            }
            //队列不满,就把新元素放到tail位置上
            data[tail]=value;
            tail++;
            //处理tail到达数组末尾的情况
            if(tail>=data.length){
                tail=0;
            }//这里的if也可以直接写tail=tail%data.length,效果一样
            size++;
            //如果入队列成功,则队列非空,就唤醒take中的阻塞等待
            locker.notify();
        }
    }

    //出队列
    public Integer take() throws InterruptedException {
        synchronized (locker){
            if(size==0){//为空不能出
                locker.wait();
            }
            //不为空,取出head位置元素
            int ret=data[head];
            head++;
            if(head>=data.length){
                head=0;
            }
            size--;
            //出队列成功之后,就唤醒put中的等待(之前满的时候要入,在wait)
            locker.notify();
            return ret;
        }
    }
}
public class Demo22 {
    public static void main(String[] args) throws InterruptedException {
        //测试
        MyBlockingQueue Queue=new MyBlockingQueue();
        Queue.put(1);
        Queue.put(2);
        Queue.put(3);
        Queue.put(4);
        int ret= Queue.take();
        System.out.println(ret);//打印1
        ret= Queue.take();
        System.out.println(ret);//2
        ret= Queue.take();
        System.out.println(ret);//3
        ret= Queue.take();
        System.out.println(ret);//4
        ret= Queue.take();
        System.out.println(ret);//一直等待,不结束进程
    }
}

运行结果如下:
在这里插入图片描述

注:
put里面如果是满的,会进行wait阻塞,然后如果有take成功了,数组里面不满,就会被take唤醒,然后进行put

take里面如果是空的,会进行wait阻塞,然后如果put成功了,数组里面不空,就会被put唤醒,然后进行take
在这里插入图片描述

可能会有同学问:如果没有等待,notify也会唤醒吗?
回答是——没有等待,notify啥也不干,没有副作用

另外,notify只能随机唤醒一个等待的线程,不能做到精准。
如果想精准,就必须使用不同的锁对象。
想唤醒t1,就o1.notify,让t1进行o1.wait
想唤醒t2,就o2.notify,让t2进行o2.wait

现在我们继续基于上面的队列实现一个生产者消费者模型:

public class Demo22 {
    private static MyBlockingQueue queue=new MyBlockingQueue();

    public static void main(String[] args) throws InterruptedException {

        //实现一个简单的生产者消费者模型

        //生产者
        Thread producer=new Thread(()->{
            int num=0;
           while(true) {
               try {
                   System.out.println("生产了"+num);
                   queue.put(num);
                   num++;
                   Thread.sleep(1000);
                    //通过sleep让生产者生产的慢一些,
                    //这样消费者就可以跟着生产者的步伐走(基本上就是生产一个消费一个)
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        producer.start();

        //消费者
        Thread customer=new Thread(()->{
           while(true){
               try {
                   int num= queue.take();
                   System.out.println("消费了"+num);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        customer.start();
    }
}

在这里插入图片描述
ps:到这里就写完了最简单的生产者消费者模式,如果你还想多几个消费者,你就再写几个线程。

三、定时器

3.1定时器是什么

定时器像是一个闹钟,进行定时。在一定时间后,(定时器)被唤醒并执行某个之前设定好的任务

我们之前学过的join(指定超时时间)和sleep(指定休眠时间)都是基于系统内部定时器来实现的。

3.2标准库中的定时器

标准库中有一个我们非常熟悉的java.util,而里面的java.util.Timer就是标准库中的定时器

java.util.Timer的核心方法就一个:schedule(安排)
也就是给定时器安排一个或者多个任务,通过这个方法,就可以把任务注册到定时器里面,然后我们给每个任务指定具体时间。于是也很好理解,我们这个方法有两个参数:1.任务是啥,2.多长时间后执行

我们先来看一下定时器的基本使用

public class Demo23 {
    public static void main(String[] args) {
        Timer timer=new Timer();
        timer.schedule(new TimerTask() {//TimerTask实现了Runnable接口
            @Override
            public void run() {
                System.out.println("我是 timer");
            }
        },3000);//第一个参数任务:说白了就是一段代码
        System.out.println("我是main");
    }
}

运行结果如下:
在这里插入图片描述
可能会有同学发现,我们的进程并没有跑结束,上面的红色方块依然存在。实际上,代码运行到这里,我们的进程没有跑完,你手动点一下红方块才会结束
在这里插入图片描述
为什么没完呢?——Timer内部有专门的线程,来负责执行注册的任务。代码走到里面任务结束之后,我们内部线程还要等待其他任务加进来。所以这个线程一直存在,从而导致进制不结束。

3.3实现定时器

要实现定时器,我们要知道一个Timer内部需要有啥东西

(1)描述任务——创建一个专门的类来表示一个定时器中的任务

//创建一个类,表示一个任务
class MyTask{
    //任务要做什么
    private Runnable runnable;
    //任务什么时候干,保存任务要执行的毫秒级时间戳
    private long time;

    public MyTask(Runnable runnable,long after){
        //after表示一个时间间隔,不是绝对的时间戳的值
        this.runnable=runnable;
        this.time=System.currentTimeMillis()+after;//从现在开始的往后after一段时间
    }

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

    public long getTime(){
        return time;
    }
}

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

比如现在有多个任务过来了:
在这里插入图片描述
我们的需求就是,能够快速的找到所有任务中,时间最小的任务——使用数据结构中的堆
ps:排序成本比较高,而且我们很可能一边执行,一边加新的任务。在这种场景中,我们最高效的数据结构就是堆

而在标准库中,有一个专门的数据结构:PriorityQueue(优先级队列,内部是个堆)

class MyTimer{
    //定时器内部要能够存放多个任务
    private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
    //此处队列要考虑线程安全问题,
    //可能在多个线程里进行注册任务,同时还有一个专门的线程来取任务执行
    //所以这里就涉及线程安全了,不能单纯的用PriorityQueue,我们用PriorityBlockingQueue

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

(3)执行时间到了的任务
先执行时间最靠前的任务,就需要一个线程,不停的去检查当前优先队列的队头元素,看看当前最靠前的这个任务是不是时间到了。
我们再在MyTimer里面加构造方法:

class MyTimer{
    //定时器内部要能够存放多个任务
    private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
    //此处队列要考虑线程安全问题,
    //可能在多个线程里进行注册任务,同时还有一个专门的线程来取任务执行
    //所以这里就涉及线程安全了,不能单纯的用PriorityQueue,我们用PriorityBlockingQueue

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

    public MyTimer(){
        Thread t=new Thread(()->{
           while(true){
               try {
                   //先取出队头元素
                   MyTask task= queue.take();
                   //再比较一下看看当前这个任务时间到了没
                   long curTime=System.currentTimeMillis();
                   if(curTime<task.getTime()){
                       //时间没到,把任务再放回队列中
                       queue.put(task);
                   }else{
                       //时间到了,或者时间已经过了(比如你要7点交作业,你7点没交,那你后面得赶紧补交)
                       task.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
    }
}

但是如果仔细分析,我们上述代码中,有两个比较严重的问题,

我们先来测试看一下第一个问题MyTask没有指定比较规则
调用代码如下,和标准库的调用是没啥区别的:

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

        System.out.println("我是main");
    }

但是运行的话,就报了的异常:
在这里插入图片描述
我们来仔细阅读一下这个异常:
在这里插入图片描述
而我们现在的代码为什么会有这种情况呢?
我们是要把元素放到优先级队列里面,而优先级队列内部又是一个堆,堆又是需要调整的,调整你就需要知道元素和元素之间的大小关系。

而我们刚才实现的MyTask这个类的比较规则,并不是默认存在的,需要我们手动指定,按照时间大小来比较。(当我们不是手动指定的时候,编译器也不知道按什么方式比较),于是就抛了异常

注:标准库中的集合类,很多都是有一定的约束和限制的,不是随便拿个类都能放到这些集合类里面去。

那我们现在让task实现compareable接口,然后重写一下compareTo方法

改进完后完整代码:

class MyTask implements Comparable<MyTask>{
    //任务要做什么
    private Runnable runnable;
    //任务什么时候干,保存任务要执行的毫秒级时间戳
    private long time;

    public MyTask(Runnable runnable,long after){
        //after表示一个时间间隔,不是绝对的时间戳的值
        this.runnable=runnable;
        this.time=System.currentTimeMillis()+after;//从现在开始的往后after一段时间
    }

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

    public long getTime(){
        return time;
    }

    @Override
    public int compareTo(MyTask o){
        return (int)(this.time-o.time);//让时间小的在前,时间大的在后
        //time是long类型,我们这里返回是int,所以强转一下
    }
}

class MyTimer{
    //定时器内部要能够存放多个任务
    private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
    //此处队列要考虑线程安全问题,
    //可能在多个线程里进行注册任务,同时还有一个专门的线程来取任务执行
    //所以这里就涉及线程安全了,不能单纯的用PriorityQueue,我们用PriorityBlockingQueue

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

    public MyTimer(){
        Thread t=new Thread(()->{
           while(true){
               try {
                   //先取出队头元素
                   MyTask task= queue.take();
                   //再比较一下看看当前这个任务时间到了没
                   long curTime=System.currentTimeMillis();
                   if(curTime<task.getTime()){
                       //时间没到,把任务再放回队列中
                       queue.put(task);
                   }else{
                       //时间到了,或者时间已经过了(比如你要7点交作业,你7点没交,那你后面得赶紧补交)
                       task.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
    }
}


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

        System.out.println("我是main");
    }
}

运行结果如下:
在这里插入图片描述
第一个问题解决了,运行结果好像和标准库里的一样,但其实还隐藏着第二个问题
在这里插入图片描述

也就是任务还没到指定的开始时间,这个线程一直在等,既没有实质效果,也没有休息好——造成资源浪费。

解决办法:可以基于wait这样的机制来实现,
wait有一个版本,指定等待时间(不需要notify,时间到了自然唤醒)
我们让它等待什么时间呢——可以计算出当前时间和任务目标时间差,就等这个时间差。

private Object locker=new Object();

    public MyTimer(){
        Thread t=new Thread(()->{
           while(true){
               try {
                   //先取出队头元素
                   MyTask task= queue.take();
                   //再比较一下看看当前这个任务时间到了没
                   long curTime=System.currentTimeMillis();
                   if(curTime<task.getTime()){
                       //时间没到,把任务再放回队列中
                       queue.put(task);
                       //指定一个等待时间
                       synchronized (locker){
                           locker.wait(task.getTime()-curTime);
                       }
                   }else{
                       //时间到了,或者时间已经过了(比如你要7点交作业,你7点没交,那你后面得赶紧补交)
                       task.run();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
    }

这里可能还有同学问,既然是指定一个等待时间,为啥不直接用sleep而是再用一下wait呢?

sleep是不能中途被唤醒的,wait能中途被唤醒。
比如你现在没有任务,你老师让你5点写作业,你定一个时间10分后写作业。但是没过一会,你老师又让你去拿个试卷,你应该立即去而不是等10分钟写作业。

在等待过程中,可能要插入新的任务,而且新的任务可能出现在之前所有任务最前面!在schedule操作中,每次新加一个任务,就需要加上一个notify操作

public void schedule(Runnable runnable,long after){
        MyTask task=new MyTask(runnable, after);
        queue.put(task);
        //每次任务插入成功之后,都唤醒一下扫描线程,
        //让线程重新检查一下队头任务,看新插入的任务是否要执行
        synchronized (locker){
            locker.notify();
        }
    }

小结:
在这里插入图片描述

四、线程池

4.1线程池是什么

说到线程池,大家一定不会陌生,我们之前也是有接触过池的概念:

进程,比较重,如果频繁创建销毁,开销就比较大。那解决方案就是进程池或者线程。

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

线程池:把线程提前创建好,放到池子里备着,如果后面需要线程,直接从池子里取,不必从系统这里申请。如果线程用完了,也不必还给系统,直接放回池子,以备下次再用,这样创建和销毁的过程速度就会更快。

那这里就会有同学问:“为什么线程放池子里,就比从系统申请释放来的快?”

操作系统中我们分两种状态:用户态、内核态
如下,是我们操作系统的软硬件结构图:
在这里插入图片描述
我们自己写的代码,有一部分就是在最上面的应用程序这一层来运行的,这里的代码称为“用户态”运行的代码。

有些代码,需要调用操作系统的API,进一步的逻辑就会在内核中执行

例如,调用一个System.out.println,本质上要经过write系统调用,进入到内核中,内核执行一堆逻辑,然后控制器输出字符串。。。

在内核中运行的代码,就称为“内核态”运行的代码

创建线程,本身就需要内核的支持(创建线程本质是在内核中搞个PCB,加到链表里),调用的Thread.start其实归根结底,也是需要进入内核态来运行。

而把创建好的线程放到“池子里”,由于池子就是用户态实现的,这个放到池子里/从池子里取,这个过程就不需要设计到内核,就是纯纯的用户态代码就能完成。

所以,一般认为,纯用户态操作,效率要比经过内核态处理的操作效率更高。

4.2标准库中的线程池

标准库的线程池叫做:ThreadPoolExecutor

它一共有4个构造方法,我们看其中参数最复杂的一个,学会这个,其他3个就迎刃而解了。

在这里插入图片描述

举例说明:
我们把线程池想象成一个公司,公司里有很多员工在干活,把员工分成两类:
1.正式员工——允许摸鱼
2.临时员工——不允许摸鱼

比如刚开始的时候,假设公司要完成的工作不多,正常员工完全能搞定,就不需要临时员工了。

如果公司的工作突然猛增,正式员工加班也搞不定,就需要雇佣一批临时工

过了一段时间,公司工作很少了,正式员工自己摸鱼也可以搞定所有工作。那临时工就没事做了,让临时工摸鱼还给他发工资,公司就会亏损了,所以就不需要这些临时员工了,要辞退。

构造方法参数:
int corePoolSize 核心线程参数(正式员工数量)

int maximumPoolSize 最大线程数(正式员工+临时员工)

long keepAliveTime 允许临时工摸鱼时间

TimeUnit unit 时间的单位(s,ms,us…)

BlockingQueue < Runnable> workQueue 任务队列
线程池会提供一个submit方法,让程序员把任务注册到线程池中,加到这个任务队列中

ThreadFactory threadFactory 线程工厂,线程是怎么创建出来的

RejectedExecutionHandle handler 拒绝策略
换句话说,当任务队列满了我们怎么做?
1.忽略最新的任务
2.阻塞等待
3.丢弃最老的任务

虽然线程池参数很多,但是使用的时候最重要的参数还是第一组参数——线程池中线程的个数。

有一个程序,这个程序要并发的/多线程的来完成一些任务,如果使用线程池的话,这里的线程数比较合适?

在这里插入图片描述
在这里插入图片描述
标准库中还提供了一个简化版本的线程池:Executors
本质是针对ThreadPoolExecutor进行了封装,提供了一些默认参数。我们来看看是怎么操作的:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo25 {
    public static void main(String[] args) {
        //创建一个固定线程数目的线程池,参数指定了线程个数(常用版本)
        ExecutorService pool= Executors.newFixedThreadPool(10);

        //创建一个自动扩容的线程池,会根据任务量自动进行扩容
        //Executions.newCachedThreadPool();

        //创建一个只有一个线程的线程池
        //Executors.newSingleThreadExecutor();

        //创建一个带有定时器功能的线程池,类似于Timer
        //Executors.newScheduledThreadPool();


        for(int i=0;i<100;i++){//100个任务,分给10个线程来完成
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello threadpool");
                }
            });
        }
    }
}

运行结果如下:
在这里插入图片描述

4.3实现线程池

我们这里仿照Executors实现一个线程池

而要实现一个线程池,就得先知道线程池里面有啥
1.能够描述任务(可直接使用Runnable)
2.能够组织任务(可直接使用BlockingQueue)
3.能够描述工作线程
4.能够组织这些线程
5.需要实现,能往线程池里添加任务

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

class MyThreadPool{
    //1.描述一个任务——直接使用Runnable

    //2.使用一个数据结构来组织若干任务——使用阻塞队列
    private BlockingQueue<Runnable> queue =new LinkedBlockingDeque<>();

    //3.描述一个线程
    static class worker extends Thread{
        //当前线程池里有若干worker线程,这些线程内部都持有了上述的任务队列
        private BlockingQueue<Runnable> queue=null;

        public worker(BlockingQueue<Runnable> queue){
            this.queue=queue;
        }

        @Override
        public void run(){
            //需要能够拿到上面的队列
            while(true){
                try {
                    //循环的去获取任务队列中的任务
                    //如果这里队列为空,就直接阻塞;如果非空,就获取里面的内容
                    Runnable runnable= queue.take();
                    //获取到之后,就去执行
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //4.创建一个数据结构来组织若干个线程
    private List<Thread> workers=new ArrayList<>();

    public MyThreadPool(int n){
        //在构造方法中,创建出若干线程,放到上述数组中
        for(int i=0;i<n;i++){
            worker worker=new worker(queue);
            worker.start();
            workers.add(worker);
        }
    }

    //5.创建一个方法,能够允许程序员来放任务到线程池中
    public void submit(Runnable runnable){
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Demo26 {//测试用例
    public static void main(String[] args) {
        MyThreadPool pool=new MyThreadPool(10);
        for(int i=0;i<100;i++){
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello ThreadPool");
                }
            });
        }
    }
}

运行结果如下,和标准库中的结果一样:
在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 内容概要 《计算机试卷1》是一份综合性的计算机基础和应用测试卷,涵盖了计算机硬件、软件、操作系统、网络、多媒体技术等多个领域的知识点。试卷括单选题和操作应用大类,单选题部分测试学生对计算机基础知识的掌握,操作应用部分则评估学生对计算机应用软件的实际操作能力。 ### 适用人群 本试卷适用于: - 计算机专业或信息技术相关专业的学生,用于课程学习或考试复习。 - 准备计算机等级考试或职业资格认证的人士,作为实战演练材料。 - 对计算机操作有兴趣的自学者,用于提升个人计算机应用技能。 - 计算机基础教育工作者,作为教学资源或出题参考。 ### 使用场景及目标 1. **学习评估**:作为学校或教育机构对学生计算机基础知识和应用技能的评估工具。 2. **自学测试**:供个人自学者检验自己对计算机知识的掌握程度和操作熟练度。 3. **职业发展**:帮助职场人士通过实际操作练习,提升计算机应用能力,增强工作竞争力。 4. **教学资源**:教师可以用于课堂教学,作为教学内容的补充或学生的课后练习。 5. **竞赛准备**:适合准备计算机相关竞赛的学生,作为强化训练和技能检测的材料。 试卷的目标是通过系统性的题目设计,帮助学生全面复习和巩固计算机基础知识,同时通过实际操作题目,提高学生解决实际问题的能力。通过本试卷的学习与练习,学生将能够更加深入地理解计算机的工作原理,掌握常用软件的使用方法,为未来的学术或职业生涯打下坚实的基础。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

劲夫学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值