JAVA面经整理(5)

JAVA面经整理(5)_第1张图片

创建线程池不是说现用先创建,而是要是可以复用线程池中的线程,就很好地避免了大量用户态和内核态的交互,不需要频繁的创建和销毁线程

JAVA面经整理(5)_第2张图片

一)什么是池化技术?什么是线程池? 

1)池化技术是提前准备好一些资源,在需要的时候可以重复使用这些准备好的资源,池化技术的优点主要有两个:提前使用和重复利用

2)线程池会先启动若干数量的线程,这些线程都会处于睡眠状态,当有一个新的任务到来的时候,线程池就会唤醒某一个睡眠的线程,让他来进行处理客户端的请求,当处理完整个请求之后,线程又处于睡眠的状态,线程池可以很好地提高程序执行的性能,因为频繁的创建和销毁线程是很浪费时间的;

3)现在假设有一个大型的网络中心,高峰期每一秒的客户端请求的并发数超过100,如果为每一个客户端都创建一个线程,那么耗费的CPU资源和内存资源都是非常惊人的,假设如果现在拥有一个数量是200的线程池,那么将会节约大量的系统资源和内存资源,使得有更多的CPU资源和内存资源来处理实际的业务,而不是把这些资源放在创建和销毁线程上;

JAVA面经整理(5)_第3张图片

1)复用线程:降低资源消耗

JAVA面经整理(5)_第4张图片

咱们的线程创建是需要开辟虚拟机栈,本地方法栈,程序计数器等一些私有的线程的内存空间,而销毁的时候又要回收这些私有空间资源,线程就可以重复的进行使用了,使得有更多的CPU资源和内存资源放在实际的业务处理上

2)提高响应速度

咱们的线程池使用已有线程来进行执行任务的,而线程当有人来的时候,现使用现创建,涉及到大量用户态和内核态的交互,所以说线程池可以更快的响应,就已经有PCB了;

3)管理线程数和任务数:线程池提供了更多的管理功能

3.1)控制最大并发数:控制最大并发线程数,线程池可以创建固定的线程数,从而避免了无限创建线程的问题,当线程创建过多的时候,会出现系统执行变慢,因为CPU核数是固定的,能够处理的任务数量也是一定的,但是当线程过多的时候,就会造成线程恶意争抢和线程频繁切换的问题,从而导致程序执行效率变慢,所以合适数量的线程才是高性能运行的关键

3.2)控制任务最大数:如果任务无限多,而内存又不足,就会导致程序执行报错,所以我们采用拒绝策略来进行处理多出的任务,从而保证系统可以健康的运行;

4)提供定期执行的功能

其他的常见的池化技术:

1)内存池:

1.1)内存池在进行创建的过程中,首先在程序启动的过程中,会向操作系统申请一大块内存

1.2)然后每次用户请求内存的时候,就会返回内存池中的一块空闲的内存,并将这块内存的标志置为true,标记为已经使用;

1.3)当内存使用完毕释放内存的时候,也不是真正地调用 free 或 delete 的过程,而是把内存放回内存池的过程,且放回的过程要把标志置为false;

1.4)最后,应用程序结束就会将内存池销毁,将内存池中的每一块内存释放

优点:减少了内存碎片的产生,提高了内存的使用频率

缺点:会造成内存的浪费,因为要使用内存池需要在一开始分配一大块闲置的内存,而这些内存不一定全部被用到

2)数据库连接池:

2.1)数据库连接池的基本思想是在系统初始化的时候将数据库连接作为对象存储在内存中,当用户需要访问数据库的时候,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象;

2.2)在使用完毕后,用户也不是将连接关闭,而是将连接放回到连接池中,以供下一个请求访问使用,而这些连接的建立、断开都是由连接池自身来管理的

2.3)同时还可以设置连接池的参数来控制连接池中的初始连接数、连接的上下限数和每个连接的最大使用次数、最大空闲时间等。当然,也可以通过连接池自身的管理机制来监视连接的数量、使用情况等

JAVA面经整理(5)_第5张图片

二)谈谈线程池的参数:

JAVA面经整理(5)_第6张图片

1)这个核心线程数如果设置成0,那么表示没有任务的时候销毁线程池,如果核心线程数大于0表示线程池即使没有任务的时候最少的线程池的数量等于这个值,如果设置比较小,那么会频繁的创建和销毁线程,如果创建的值比较大,那么会浪费大量的系统资源
2)如果线程池空闲的时间超过了此时间,那么多余的线程就会被销毁,直到线程池中的线程的销毁数量等于corepoolsize,如果本身corepoolsize等于maxmunsize,那么线程池在空闲的时候也不会销毁任何线程JAVA面经整理(5)_第7张图片

JAVA面经整理(5)_第8张图片

三)如何设置corePoolsize maximumpoolsize之间的关系呢?

1)如果解决的任务和任务量比较稳定,我们会把corePoolsize和maximumpoolsize

之间的关系设得会比较接近,让临时工尽量少一些;

2)但是如果解决的任务场景,任务量波动比较大,可以让corePoolsize和maximumpoolsize之间的差值大一些,多多的进行招临时工,让临时工的数量多一些;

3)这个题太开放了,和机器,执行任务的类型有关,还是要通过实验来进行,从节省资源和执行效率的角度进行权衡,通过实验的方式来进行测试比较合适;

4)为什么线程池里面还要销毁线程?

1)这是在性能和资源之间在做平衡,线程完全不销毁,相当于是性能最大化,创建线程销毁线程的次数最少,但是吃的资源也是最多的,尤其是内存资源;

2)线程不消毁,线程越多,只是把性能跳到了最大限度,但是这些线程也会占用一定的资源,如果任务量更少,进程不干活,拜拜,把内存资源节省出来给其他工作去使用

3)其中的keepAlivetime,的时间越短,希望吃的资源越少,时间越短,就可以更快地释放资源,临时工线程就可以早早得进行走人,线程就可以及时地释放,把系统资源让出来;

进行使用多线程,不就是说为了让程序跑得更快吗?但是咱们为啥不让CPU占用率太高呢?

对于线上服务器来说,要对CPU资源留有一定的冗余,随时应对一些可能的突发情况,如果说请求已经本身已经快把CPU占用完了,这个时候突然来了一波请求的峰值,请求暴涨,此时我们的服务器很有可能就会挂

5)如何设置线程池中线程的个数? 

有一个程序,这个程序要进行并发的,多线程来进行处理一些任务,如果使用线程池的话,这里面的线程数我们进行设置成多少是比较合适的呢?

1)针对这个问题,不能给出一个具体的值,只要是能够回答出一个具体的数字,那么一定是错误的

2)我们只能通过一系列的方法来找到一个具体的值,而不是说直接再进行测试之前,就直接进行拍板子一定就行的

正确的方式:要通过性能测试的方式,来找到合适的值

1)比如说可以写一个服务器程序,服务器里面通过线程池,多线程的处理用户请求

我们就可以针对这个服务器进行性能测试,比如说构造一些请求,发送给服务器,因为是要要进行测试性能,所以这里面的构造请求就需要构造很多,比如说每秒构造500/1000/2000请求,根据实际的业务场景,来进行构建一个合适的值;

2)我们再进行性能测试的时候,我们就可以根据不同的线程池中的线程数,来进行观察两个指标,程序处理任务的速度,程序持有的CPU的占用率

2.1)当我们的线程数多了,整体的速度会变快,但是CPU的占用率也会变高

2.2)当我们的线程数少了,整体的速度会变慢,但是CPU的占有率也会下降

我们需要在合适的情况下找到一个平衡点,所以说应该需要找到一个让程序速度可以接受,并且CPU占用也合理的这样一个平衡点,这才是我们正确的做法

2.2)因为不同类型的程序,因为单个任务,里面CPU的计算的时间和阻塞的时间分布是不相同的,因此光去拍脑门进行设置是不可以的,一定要进行性能测试

6)在JAVA中创建线程池有哪几种方式? 

大体上创建线程池一共有两种方式,要么通过ThreadPoolExcutor创建线程池,要么通过Excutors执行器创建线程池

JAVA面经整理(5)_第9张图片

1)Executors.newFixedThreadPool:创建一个固定数量的线程池,可控制并发的线程数,超出的任务会在队列中等待,建出一个固定线程数量的线程池,完全没有零时工的版本,参数指定了线程的个数

  //1.创建一个固定大小的线程池,可以控制并发线程数
        ExecutorService threadPool= Executors.newFixedThreadPool(2);
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                System.out.println("任务被执行"+Thread.currentThread().getName());
            }
        };
        threadPool.submit(runnable);
        threadPool.submit(runnable);
        threadPool.submit(runnable);
        threadPool.submit(runnable);
//打印结果:
任务被执行pool-1-thread-2
任务被执行pool-1-thread-1
任务被执行pool-1-thread-2
任务被执行pool-1-thread-1
//先执行两个任务,剩余的任务直接进行排队执行

2)Exectors.newCatchedThreadPool:创建出了一个数量可变的线程池,完全没有正式员工,全是临时工,是一个自动扩容的线程池,会根据任务量来自动进行扩容,如果线程数超过处理所需,那么多余的线程会进行缓存一会被回收,如果线程数不够就新创建线程

创建出了一个数量可变的线程池,完全没有正式员工,全是临时工,是一个自动扩容的线程池,会根据短时间内的任务量来自动进行扩容,适合短时间内有大量突发任务的场景

package OperateNode;

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

public class ExectorOperator {
    public static void main(String[] args) {
      ExecutorService service=Executors.newCachedThreadPool();
      for(int i=0;i<10;i++){
          service.submit(new Runnable() {
              @Override
              public void run() {
                  System.out.println("线程池正在创建任务"+Thread.currentThread().getName());
              }
          });
      }
    }
}
打印结果:
线程池正在创建任务pool-1-thread-2
线程池正在创建任务pool-1-thread-1
线程池正在创建任务pool-1-thread-4
线程池正在创建任务pool-1-thread-3
线程池正在创建任务pool-1-thread-7
线程池正在创建任务pool-1-thread-5
线程池正在创建任务pool-1-thread-9
线程池正在创建任务pool-1-thread-6
线程池正在创建任务pool-1-thread-10
线程池正在创建任务pool-1-thread-8
从上面可以看出线程池创建了10个线程来进行执行相应的任务

3)ScheduleExecutorServiceservice=Exectors.ScheduledThreadPool

newScheduleThreadPool:能够设定延长时间的线程池,插入的任务能够过一会再执行,相当于进阶版的定时器,带有定时器功能的线程池;

   ScheduledExecutorService service=Executors.newScheduledThreadPool(5);
        //添加定时任务1s后执行
        System.out.println(new Timestamp(System.currentTimeMillis()));
        service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("我是大佬");
            }
        },1,TimeUnit.SECONDS);
        System.out.println(new Timestamp(System.currentTimeMillis()));
    }
}

4)Exectors.newSingleThreadScheduledExector

创建一个单线程并且可以执行延迟任务的线程池;

  ScheduledExecutorService service =Executors.newSingleThreadScheduledExecutor();
        service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("大佬");
            }
        },2, TimeUnit.SECONDS);
    }

5)Exectors.newSingleThreadExecutor:创建一个单个线程数的线程池,拥有任务队列,保证任务先进先出的执行顺序

package OperateNode;

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

public class ExectorOperator {
    public static void main(String[] args) {
        ExecutorService service= Executors.newSingleThreadExecutor();
        for(int i=0;i<10;i++){
            final int index=i;
            service.submit(new Runnable() {
                @Override
                public void run() {
           System.out.println("当前任务"+index+"正在被执行"+Thread.currentThread().getName());
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
打印结果为:
当前任务0正在被执行pool-1-thread-1
当前任务1正在被执行pool-1-thread-1
当前任务2正在被执行pool-1-thread-1
当前任务3正在被执行pool-1-thread-1
当前任务4正在被执行pool-1-thread-1
当前任务5正在被执行pool-1-thread-1
当前任务6正在被执行pool-1-thread-1
当前任务7正在被执行pool-1-thread-1
当前任务8正在被执行pool-1-thread-1
当前任务9正在被执行pool-1-thread-1
public static void main(String[] args) {
        ExecutorService service= Executors.newSingleThreadExecutor();
        for(int i=0;i<10;i++){
            final int index=i;
            service.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("当前线程池正在运行第"+index+"个任务");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

那么单个线程的线程池有什么意义呢?

1)可以进行复用线程,即使是单个线程池也可以进行复用线程

2)提供了任务管理功能,单个线程池也有任务队列,在任务队列可以进行存储多个任务,并且当任务队列满了之后,保证任务顺序执行,可以执行拒绝策略,这些都是线程所不会具备的;

6)创建一个抢占式执行的线程池:Exectors.newWorkStealingPool:创建一个抢占式执行的线程池,就是任务执行顺序不确定,在JDK1.8中才可以进行使用

public class ExectorOperator {
    public static void main(String[] args) {
        ExecutorService service=Executors.newWorkStealingPool();
        //下面是执行流程
        for(int i=0;i<10;i++){
            final int index=i;
            service.submit(new Runnable() {
                @Override
                public void run() {
                   System.out.println(index+"被"+Thread.currentThread().getName()+"执行");
                }
            });
        }
     while(!service.isTerminated()){
//确保任务完成
     }
    }}
7)为什么创建线程池一定要使用ThreadPoolExecutor?

ThreadPool是最原始也是最推荐的手动创建线程池的方式,可以通过参数来进行控制最大任务数和最大线程数和拒绝策略,让线程池的执行更加透明和可控,明确线程池的运行规则,规避资源耗尽的风险

1)FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM

2)CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

8)手动创建线程池
9)谈谈工厂模式: 

你可能感兴趣的:(java,开发语言)