目录
5、面试题:线程池中阻塞队列作用?为什么是先添加列队而不是先创建最大线程?
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。
许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
举一个例子,网站的计数器,一般也是采用单例模式实现,如果你存在多个计数器,每一个用户的访问都刷新计数器的值,这样的话你的实计数的值是难以同步的。但是如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。同样多线程的线程池的设计一般也是采用单例模式,这是由于线程池需要方便对池中的线程进行控制
适用场景:
优点:
缺点:
- public class Singleton1 {
- private static Singleton1 instance = new Singleton1();
-
- private Singleton1(){}
-
- public static Singleton1 getInstance(){
- return instance;
- }
- }
类加载的方式是按需加载,且加载一次。。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例,这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
- public class Singleton2 {
- private static Singleton2 instance;
-
- private Singleton2(){}
-
- public static Singleton2 getInstance(){
- if (instance == null) {
- instance = new Singleton2();
- }
- return instance;
- }
- }
我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。
这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
- public class Singleton {
- private volatile static Singleton instance;
- private Singleton (){}
- public static Singleton getInstance() {
- if (instance == null) {
- synchronized (Singleton.class) {
- if (instance == null) {
- instance = new Singleton();
- }
- }
- }
- return instance;
- }
- }
双锁模式,进行了两次的判断,第一次是判断是否要加锁,第二次是判断是否要创建实例。由于singleton=new Singleton()对象的创建在JVM中可能会进行重排序,在多线程访问下存在风险,使用volatile修饰signleton实例变量有效,解决该问题。
从这几种实现中,我们可以总结出,要想实现效率高的线程安全的单例,我们必须注意以下两点:
阻塞队列同样遵守“先进先出”的原则
阻塞队列的一种线程安全的数据结构,具有以下特性:
生产者消费者模型——指的是多线程协同工作的一种方式
例如:流水线工作中,A处处理的半成品,需要送往B处进行再次处理,此时A、B就是两个不同的线程,如图不使用消息队列:

劣势:A、B之间的衔接,通信时,耦合度比较高,处理代码A、B时,必须知道彼此的存在,倘若代码已经写好了,此时又要加上一个与B同级的C处理,那么此时,还必须得修改A处的代码
(1、使用阻塞队列,有利于代码的“解耦合”
如图:

使用阻塞队列作为A、B之间的一个交易场所,此时处理A、B代码时,就不需要考虑对方了,A只管往阻塞队列中插入数据,B只管取出元素
(2、削峰填谷
当A处理的产品突增时,

使用阻塞队列:
使用生产者消费者模型,那么即使A请求暴涨,也不会影响到B,顶多A挂了,应用服务器不会受到影响,这是因为A请求暴涨后,用户的请求都被打包到阻塞队列中(如果阻塞队列有界,则会引起队列阻塞,不会影响到B),B还是以相同的速度处理这些请求,所以生产者消费者模型可以起到“削峰填谷”的作用。

(1、标准库的阻塞队列
- public class Test1 {
- public static void main(String[] args) throws InterruptedException {
- //队列的三个基本操作,入队列,出队列,取队首元素
- BlockingQueue
queue = new LinkedBlockingQueue<>(); - //入队列,put带有阻塞功能,而offer没有
- queue.put(1);
- queue.put(2);
- queue.put(3);
- //出队列
- System.out.println("出队列:"+queue.take());
- //阻塞队列没有提供“带有阻塞功能”的取队首元素的方法
- }
- }
(2、使用循环队列实现阻塞队列
- class MyBlockingQueue {
- private int[] item = new int[1000];
- private volatile int head = 0;//记录队首
- private volatile int tail = 0;//记录队尾
- private volatile int size = 0;//记录元素个数
-
- //入队列
- public void put(int elem) throws InterruptedException {
- synchronized (this) {
- //判断队列是否满了
- while (size >= item.length) {
- //return;
- //阻塞
- this.wait();
- }
- //未满
- item[tail] = elem;
- tail++;
- size++;
- if (tail >= item.length) {
- tail = 0;
- }
- this.notify();
- }
- }
- //出队列
- public Integer take() throws InterruptedException {
- synchronized (this) {
- //判断队列是否为空
- while (size == 0) {
- //return null;
- //阻塞
- this.wait();
- }
- //不为空
- int val = item[head];
- head++;
- if (head >= item.length) {
- head = 0;
- }
- size--;
- this.notify();
- return val;
- }
- }
-
- }
定时器其实就相当于一个闹钟,当闹钟响了,就执行相应的任务
- public class Test1 {
- public static void main(String[] args) {
- Timer timer = new Timer();
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- System.out.println("时间到,开始执行任务1");//run()方法里面是要执行的任务
- }
- },3000);//3000是自己任意指定的时间间隔
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- System.out.println("时间到,开始执行任务2");
- }
- },5000);
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- System.out.println("时间到,执行任务3");
- }
- },1000);
- System.out.println("开始计时");
- }
- }
运行结果:

(1、首先呢,创建一个类,表示任务:
- class MyTask {
- //任务
- private Runnable runnable;
- //时间
- private long time;
-
- public MyTask(Runnable runnable,long time) {
- this.runnable = runnable;
- this.time = System.currentTimeMillis()+time;
- }
-
- }
(2、模拟一个Timer类:
- class MyTimer {
- public void schedule(Runnable runnable,long time) {
- MyTask myTask = new MyTask(runnable,time);
- }
- }
模拟到这里,一个大致框架已经出来了,接下来,我们需要思考,如何去管这些任务,根据时间的前后顺序来依次执行任务,我们都会想到可以使用优先级队列来实现 ,没错,确实是这样子的,但同时我们需要注意一个点,优先级队列是线程不安全的,为保证线程安全,我们采用以下方式(结合阻塞队列):
private BlockingQueue queue = new PriorityBlockingQueue<>();
(3、创一个扫描线程,检查队首元素的时间是否到了,如果到了,则执行该任务
- public MyTimer() {
- Thread t = new Thread(() -> {
- while(true) {
- //取出队首元素
- try {
- MyTask myTask = queue.take();
- if(myTask.getTime() <= System.currentTimeMillis()) {
- //到点执行任务
- myTask.getRunnable().run();
- } else {
- queue.put(myTask);//塞回队列
- }
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
- });
- }
(4、添加对象之间比较的方法
- public int compareTo(MyTask o) {
- return (int)(this.time - o.time);
- }
大致这样算是模拟出来了
(5、优化
优化一:线程安全问题
优化二:上述代码中循环判断任务执行的时间是否到了,导致CPU没有空做其他时间
完整代码:
- //用这个类表示一个任务
- class MyTask implements Comparable
{ - //任务
- private Runnable runnable;
- //时间
- private long time;
-
- public MyTask(Runnable runnable,long time) {
- this.runnable = runnable;
- this.time = System.currentTimeMillis()+time;
- }
-
- public Runnable getRunnable() {
- return runnable;
- }
-
- public long getTime() {
- return time;
- }
- public int compareTo(MyTask o) {
- return (int)(this.time - o.time);
- }
- }
- class MyTimer {
- private BlockingQueue
queue = new PriorityBlockingQueue<>(); -
- private Object locker = new Object();
- public MyTimer() {
- Thread t = new Thread(() -> {
- while(true) {
- //取出队首元素
- try {
- synchronized (locker) {
- MyTask myTask = queue.take();
- long curTime = System.currentTimeMillis();
- if (curTime >= myTask.getTime()) {
- //到点执行任务
- myTask.getRunnable().run();
- } else {
- queue.put(myTask);//塞回队列
- locker.wait(myTask.getTime() - curTime);
- }
- }
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
- });
- t.start();
- }
- public void schedule(Runnable runnable,long time) throws InterruptedException {
-
- MyTask myTask = new MyTask(runnable,time);
- queue.put(myTask);
- synchronized (locker) {
- locker.notify();
- }
- }
- }
- public class Test3 {
- public static void main(String[] args) throws InterruptedException {
- MyTimer timer = new MyTimer();
- timer.schedule(new Runnable() {
- @Override
- public void run() {
- System.out.println("时间到执行任务1");
- }
- },1000);
- timer.schedule(new Runnable() {
- @Override
- public void run() {
- System.out.println("时间到执行任务2");
- }
- },1500);
- timer.schedule(new Runnable() {
- @Override
- public void run() {
- System.out.println("时间到执行任务3");
- }
- },500);
- }
- }
理解池——池,就是鱼塘咯!比如,一家柠檬鱼的饭店,用鱼量比较大,每次有客人来饭店吃饭,都需要派人去买鱼,那就太浪费人力物力和时间了。怎么解决呢?在饭店的后院修一个小鱼塘,一次性买个1000条鱼放在鱼塘里,来客人了,直接从鱼塘中拿鱼,节省时间,节省开销了。
同理,线程池就是池子里面放很多已经创建好了的线程,使用时,直接从池子中取出它,使用即可,使用完了,将线程还回到池子中
使用线程池是纯用户态操作,要比创建线程(经历内核态)要快
为什么用线程池?
java也提供了相关行程池的标准类ThreadPoolExecutor,也被称作多线程执行器,该类里面的线程包括两类,一类是核心线程,另一类是非核心线程,当核心线程全部跑满了还不能满足程序运行的需求,就会启用非核心线程,直到任务量少了,慢慢地,非核心线程也就退役了,通俗一点核心线程就相当于公司里面的正式工,非核心线程相当于临时工,当公司人手不够的时候就会请临时工来助力工作,当员工富余了,公司就会将临时工辞退。
jdk8中,提供了4个构造方法,我主要介绍参数最多的那一个构造方法,其他3个构造方法都是基于此构造方法减少了参数,所以搞懂最多参数的构造方法,其他构造方法也就明白了。
- //参数最多的一个构造方法
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue
workQueue, - ThreadFactory threadFactory,
- RejectedExecutionHandler handler)

另几个构造方法:
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue
workQueue) -
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue
workQueue, - ThreadFactory threadFactory)
-
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue
workQueue, - RejectedExecutionHandler handler)
重点(面试问题):
那核心线程数最合适值是多少呢?假设CPU有N核心,最适核心线程数是N?是2N?是1.5N?只要你能够说出一个具体的数,那就错了,最适的核心线程数要视情况而定,没有一个绝对的标准的值。
在具体使用线程池时,往往使用的是Executor,因为 Executor是 ThreadPoolExecutor所实现的一个接口,由于标准库中的线程池使用较复杂,对于ThreadPoolExecutor类中的方法我们就不介绍了,最重要的一个方法是submit方法,这个方法能够将任务交给线程池去执行,接下来我们来理一理线程池最基本的工作原理,我们来尝试实现一个简单的线程池(继续往下看)。
- public class Test {
- public static void main(String[] args) {
- ExecutorService pool = Executors.newCachedThreadPool();
- pool.submit(new Runnable() {
- @Override
- public void run() {
- System.out.println("任务");
- }
- });
- }
- }
4.1、线程池的基本工作原理
线程池是通过管理一系列的线程来执行程序员所传入的任务,这些任务被放在线程池对象中的一个阻塞队列中,然后线程池会调度线程来执行这些任务,优先调度核心线程(核心线程会在线程池对象构造时全部创建),如果核心线程不够用了,就会创建非核心线程来帮忙处理任务,当非核心线程一定的时间没有收到新任务时,非核心线程就会被销毁,我们实现线程池的目的是加深对线程池的理解,所以实现的过程中就不去实现非核心线程了,线程池里面的线程全部以核心线程的形式实现。
我们需要实现一个线程池,根据以上的原理需要准备:
submit。4.2、 线程池的简单实现
关于任务和任务的组织就不用多说了,直接使用Runnable和阻塞队列BlockingQueue
然后根据传入的核心线程数来创建并启动工作线程,将这些线程放入顺序表或链表中,便于管理。
最后就是创建一个submit方法用来给用户或程序员派发任务到阻塞队列,这样线程池中的线程就会去执行我们所传入的任务了
代码:
- class MyThreadPool {
- //1.需要一个类来描述具体的任务,直接使用Runnable即可
- //2.有了任务,我们需要将多个任务组织起来,可以使用阻塞队列
- private final BlockingQueue
queue = new LinkedBlockingQueue<>(); -
- //3.组织好任务,就可以分配线程池中的线程来执行任务了,所以我们需要描述线程,专门来执行任务
- static class Worker extends Thread {
- //获取任务队列
- private final BlockingQueue
queue; - //构造线程时需要将任务队列初始化
- public Worker(BlockingQueue
queue) { - this.queue = queue;
- }
- //重写线程中的run方法,用来执行阻塞队列中的任务
- @Override
- public void run() {
- while (true) {
- try {
- //获取任务
- Runnable runnable = queue.take();
- //执行任务
- runnable.run();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- // 4.线程池中肯定存在不止一个线程,所以我们需要对线程进行组织,这里我们可以使用顺序表,使用链表也可以
- private final List
workers = new ArrayList<>(); -
- //根据构造方法指定的线程数将线程存入workers中
- public MyThreadPool(int threadNums) {
- for (int i = 0; i < threadNums; i++) {
- Worker worker = new Worker(this.queue);
- worker.start();
- this.workers.add(worker);
- }
- }
- // 5.创建一个方法,用来将任务存放到线程池中
- public void submit(Runnable runnable) {
- try {
- this.queue.put(runnable);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
(1)、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前任务了,就无法保留当前的任务了,阻塞队列通过阻塞可以保留当前想要继续入队的任务
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,得到线程进入wait状态,释放CPU资源
阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用CPU资源
(2)、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率.
| static class | ThreadPoolExecutor.AbortPolicy 被拒绝的任务的处理程序,抛出一个 (好比领导给员工安排了很多活,加班都干不完,这个员工干不下去了,一瞬间就情绪崩溃,晕倒了;) |
| static class | ThreadPoolExecutor.CallerRunsPolicy ( 好比领导给员工安排了很多任务,员工干不过来,员工就给领导说,你来干吧,这时候领导自己干,如果能干他就干了,不能干,就丢弃这个任务了;) |
| static class | ThreadPoolExecutor.DiscardOldestPolicy (好比领导个员工安排了任务1、任务2、任务3,领导安排到任务4的时候,员工说谈干不了,领导就说,没事,任务1不着急,你先干任务4;) |
| static class | ThreadPoolExecutor.DiscardPolicy 被拒绝的任务的处理程序静默地丢弃被拒绝的任务。 (好比领导个员工安排了任务1、任务2、任务3,领导安排到任务4的时候,员工说谈干不了,领导说,没事,任务4不着急,你先干之前的吧;) |