JAVAEE---多线程5

面试常考题

案例三:定时器

这个定时器--->就像一个闹钟,进行定时,在一定时间后被唤醒,并执行某个之前设定好的任务

我们先来介绍标准库的定时器用法

java.util.Timer核心方法就一个----->schedule----->有两个参数:任务是什么;多长时间之后执行

JAVAEE---多线程5_第1张图片

由于Timer内部有专门的线程来负责执行注册的任务的,那么接下来我们来看下Timer内部都需要什么东西吧(共三个

1.描述任务

创建一个专门的类来表示一个定时器中的任务(TimerTask)

 JAVAEE---多线程5_第2张图片

 2.组织任务

使用一定的数据结构,把一些任务组织起来

这里我们举一个例子:

假设你现在有3个任务---->一个小时之后去做作业;

                                          三个小时之后去上课;

                                          10分钟之后休息一会儿

安排任务的时候,这些任务的顺序是无序的,但是执行任务的时候就得是有序的了,要按照时间先后来执行--------->我们需要快速的找到所有的任务中时间最小的任务

组织任务需要使用一定的数据结构,那么根据刚刚上面的例子,我们不难看出,这里需要使用的数据结构是------>在标准库中,有一个专门的数据结构---->PriorityQueue------->但是这里我们选一个------>PriorityBlockingQueue----->原因是我们的操作中会有出队列,也有入队列操作,因此需要阻塞队列----->而这个PriorityBlockingQueue既带有优先级又带有阻塞队列

 3.执行时间到了的任务

既然我们需要先执行时间靠前的任务,那么就需要有一个线程不停的检查当前优先队列的队首元素,看看当前对靠前的任务是不是时间到了

到此,上面的代码还有两个缺陷,需要我们解决

第一个缺陷---->因为MyTask类的比较规则并不是默认就存在的

决绝方案---->因此需要我们手动指定,我们需要在MyTask类中加上Comparable接口----->

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

第二个缺陷---->队列这里如果不加任何限制,循环就会执行得很快

如果队列中的任务是空的还好,这个线程就在这里阻塞了,但就怕队列中的任务不空,并且任务时间还没到(此时就成了--->忙等,既没有实质性的工作产出,同时又没有进行休息)

JAVAEE---多线程5_第3张图片

解决方案---->使用wait机制---->解释一下,wait有一个版本,指定等待时间(不需要notify,时间到了自然唤醒)----->计算出:当前时间和任务的目标之间的时间差 就=等待多长时间

这里你可能会问,既然是指定一个等待时间,为什么不直接用sleep,而是用wait呢?

回答---->由于在等待过程中,可能要插入新的任务!新的任务是可能出现在所有任务的最前面的!sleep不能被中途唤醒,而wait能够被中途唤醒!这个时候,在schedule操作中就要加上一个notify操作,因此使用wait

到此,两个缺陷也填补上了

我们的案例三:定时器案例也介绍完了

简单总结下流程步骤:

以下是全代码

package thread;

import java.util.PriorityQueue;
import java.util.concurrent.PriorityBlockingQueue;

// 创建一个类, 表示一个任务.
class MyTask implements Comparable {
    // 任务具体要干啥
    private Runnable runnable;
    // 任务具体啥时候干. 保存任务要执行的毫秒级时间戳
    private long time;

    // after 是一个时间间隔. 不是绝对的时间戳的值
    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 {
    // 定时器内部要能够存放多个任务
    private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable, long delay) {
        MyTask task = new MyTask(runnable, delay);
        queue.put(task);
        // 每次任务插入成功之后, 都唤醒一下扫描线程, 让线程重新检查一下队首的任务看是否时间到要执行~~
        synchronized (locker) {
            locker.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 {
                        // 时间到了, 执行这个任务
                        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("hello timer!");
            }
        }, 3000);
        System.out.println("main");
    }
}

面试常考题

案例四:线程池

我们知道进程比较重,频繁创建销毁,开销很大,因此我们需要进程池or线程这样的解决方法

而线程呢,虽然比进程轻,但如果创建销毁的频率进一步增加,仍然会发现开销还是有的,那么我们这里的解决方案是------->线程池or协程

我们来介绍下线程池~~

(我们知道使用线程池就是,把线程提前创建好,放到池子中,后面需要用线程,直接从池子里取,不必从系统申请;线程用完了就直接放回池子中,也不用还给系统,以备下次使用)

把线程放在池子里,比从系统申请释放线程速度更快---->但是为什么呢?我们简单解释下

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

而有些代码需要调用操作系统的API,进一步的的逻辑会在内核中执行,在内核中运行的代码称为“内核态”运行的代码

我们创建线程需要内核的支持(创建线程本质是在内核中搞个PCB,加到链表里),而如果我们进行把创建好的线程放到“池子”里或是从池子中取线程的操作(由于池子是用户态实现的),这个过程不需要涉及到内核态,因此就靠纯户态就可以了

由于纯用户态的操作效率比经过内核处理的操作效率高,因此我们说把线程放在池子里,比从系统申请释放线程速度更快

下面我们学习一下java标准库中线程池的使用

标准库的线程池叫做:ThreadPoolExecutor 

 看上面这个构造方法,我们把这里面的参数逐一介绍一下~

这两个参数我们比较常用,因此一起说

JAVAEE---多线程5_第4张图片

其他的参数:

 

JAVAEE---多线程5_第5张图片

JAVAEE---多线程5_第6张图片

面试题:有一个程序,这个程序要并发的/多线程的完成一些任务。如果使用线程池的话,这里的线程数设为多少合适?

答案通过性能测试的方式,找到合适的值(记住,只要回答是一个具体的数字,都一定是错的!)

举一个例子:写一个服务器程序,服务器里通过线程池多线程的处理用户请求,就可以对这个服务器进行性能测试,比如说构造一些请求,发送给服务器,用以测试性能.根据实际的业务场景,构造一个合适的值.

根据不同的线程池的线程数来观察程序处理任务的速度和程序持有的CPU的占用率(当线程数多了,整体的速度就会变快,但CPU占用率也会高;当线程数少了,整体的速度就会变慢,但CPU占用率也会下降).我们需要找到一个让程序速度能接受,并且CPU占用也合理的平衡点.不同类型的程序,因为单个任务里面CPU上计算的时间和阻塞的时间是分布不相同的,因此会打出一个具体的数字就是不靠谱的

标准库中还提供了一些简化版本的线程池---->Executors(本质是针对ThreadPoolExecutor进行了封装,提供了一些默认参数)

下面我们来实现一个Executors这样的线程池

​​​​​​​

​​​​​​​JAVAEE---多线程5_第7张图片

package thread;

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

class MyThreadPool {
    // 1. 描述一个任务. 直接使用 Runnable, 不需要额外创建类了.
    // 2. 使用一个数据结构来组织若干个任务.
    private BlockingQueue queue = new LinkedBlockingQueue<>();
    // 3. 描述一个线程, 工作线程的功能就是从任务队列中取任务并执行.
    static class Worker extends Thread {
        // 当前线程池中有若干个 Worker 线程~~ 这些 线程内部 都持有了上述的任务队列.
        private BlockingQueue queue = null;

        public Worker(BlockingQueue 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 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");
                }
            });
        }
    }
}

你可能感兴趣的:(JAVAEE,java-ee)