面试问题-2

今天是2021-2-28

一。new一个对象的步骤

想要new一个对象,首先要看这个对象对应的类是否以及被加载到jvm内存中。如果没有,那么先加载对应的class文件到jvm中再创建对象。

   Person p=new Person(...);

假设我们是第一次使用person类,那么首先加载person的class文件到jvm:
1.类加载器接到加载person类的命令,经过双亲委派机制加载person类的class文件到jvm运行时数据区
2.将class文件放到class文件常量池中,并生成一个全局唯一的person类的class类实例
3.验证class文件是否合法
4.为person类中所有的静态变量分配内存,并赋对应类型的默认值,基本数据类型赋对应类型值,引用数据类型赋null。如果一个静态变量被final修饰,那么直接执行赋值语句。
5.将class文件中所有的直接引用解析为符号引用,包含对字段、类/接口、方法的引用。解析完成的符号引用,都会存放在运行时常量池中。也就是说,一个class文件对应一个运行时常量池。
6.以先父后子的顺序,执行每个类的构造器。构造器包含这个类所有的静态变量的赋值语句和静态代码块,执行每个静态变量的赋值语句,再执行代码块的语句。
6.1类的构造器只会在加载class文件到运行时数据区时执行,只执行一次。多线程环境下,jvm会保证只有一个线程来执行某个类的构造器。
此时,person类已经加载完成,开始创建对象
1.新创建的对象都会分配在新生代的eden区。eden区的指针移动,尝试给即将创建的对象分配内存。如果内存不够,会引发Minor GC,有可能进一步引发Full GC。
1.1在多线程环境下,可能同时有多个线程使用eden区的指针尝试分配内存空间。对此,jvm采用CAS配上失败自旋的方式保证了更新操作的原子性。
1.2另一种解决方案:内存分配的动作按照线程划分在不同的空间中进行: 为每个线程在Java堆中预先分配一小块内存 ,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。
2.在经过可能的GC后,对象的内存分配完成。根据元空间的运行时常量池中person类的定义,为内存中person类的属性赋默认值
3.jvm对对象头进行必要的设置 ,例如这个对象是哪个类的实例(即所属类)、如何才能找到类的元数据信息、对象的hashcode、对象的GC分代年龄等信息,这些信息都存放在对象的对象头中。
4.如果属性有显示的初始化赋值语句,比如int i=10,先执行赋值语句
5.调用person类的构造方法,根据传入的值对属性赋值。new Person(…)完成
6.在栈空间创建person类的引用p,即Person p
7.将堆空间的内存空间的地址赋给p,即Person p=new Person(…),对象创建完成。

二。线程池

线程池常用参数
1.corepoolsize:线程池的核心线程数量
2.maximumPoolSize(线程池的最大数量): 线程池允许创建的最大线程数。如果阻塞队列已满,线程数小于maximumPoolSize便可以创建新的线程执行任务。
3.workQueue(工作队列): 用于保存等待执行的任务的阻塞队列。
4.keepAliveTime:线程活动保持时间
5.handler(饱和策略,或者又称拒绝策略):当前无法处理新提交的任务,必须采取一种策略处理提交的新任务。
corepoolsize为什么被叫做初始大小
对于四种线程池来说,当一个新任务提交到线程池时,如果当前运行的线程数小于核心线程数,那么就要创建一个新的工作线程来执行这个提交的任务。
线程池初始化时会直接创建corepoolsize个线程吗/提交的任务什么时候会放到队列中
不会,corepoolsize仅作为一个判断标准。对于FixedThreadPool、SingleThreadExecutor来说,提交任务时如果发现当前运行的线程数等于corepoolsize,就会将提交的任务放到阻塞队列中。
线程池线程如何复用
线程池在创建线程时,会将线程封装成工作线程Worker。Worker在执行完任务后,不是立即销毁而是循环获取阻塞队列里的任务来执行。
线程池工作流程/原理,在工作过程中常用的参数都有什么作用
要讲线程池工作流程,还是四种线程池分开讲。线程池分为两类,第一种是ThreadPoolExcecutor(普通线程池),第二种是ScheduledThreadPoolExcecutor(定时线程池)
普通线程池又分为:CachedThreadPool、FixedThreadPool、SingleThreadExecutor。
CachedThreadPool:
(1)使用不能存放元素的阻塞队列SynchronousQueue
(2)核心线程数为0,最大线程数为Integer.MAX_VALUE,就是说不限制最大线程数
(3)该线程池创建的线程最多空闲60秒
工作原理:当一个任务提交到该线程池时,向队列提交任务–offer(),注意该线程池使用的队列不存储任务,需要线程马上执行。当前有空闲线程在执行poll(),那么就由这个线程执行任务;如果当前没有空闲线程,意味着offer()无法与poll()配对,那么就新建一个工作线程执行任务。工作线程执行完当前任务后会执行poll(),等待60秒。60秒内有新任务提价,将会执行提交的任务。超过60秒还没新任务可执行,这个线程会被回收。
FixedThreadPool:
(1)使用基于链表的无界队列LinkedBlockingQueue,可以存放无限个任务,所以线程池中的线程数永远不会大于核心线程数
(2)核心线程数和最大线程数相同,在创建时指定
(3)该线程池创建的线程空闲时间为0
工作原理:当一个任务提交到该线程池时。如果当前线程数小于核心线程数,创建新的工作线程池执行该任务。如果当前线程数等于核心线程数,将该任务放入阻塞队列中。
工作线程执行完任务后,会循环从队列中获取任务继续执行。
SingleThreadExecutor:
(1)使用基于链表的无界队列LinkedBlockingQueue,可以存放无限个任务
(2)核心线程数和最大线程数都设置为1
(3)该线程池创建的线程空闲时间为0
工作原理:当一个任务提交到该线程池时。如果当前线程池中没有线程,创建一个线程执行提交的任务。如果当前池中已有一个线程,将任务放入阻塞
队列中。工作线程执行完任务后,会循环从队列中获取任务继续执行。
ScheduledThreadPoolExcecutor
(1)使用无界队列DelayQueue
DelayQueue包含以下几个组件

  1. PriorityBlokingQueue,一个支持优先级的无界阻塞队列。该队列会排序添加进来的task。time小的排在前面,time相同按sequenceNumber排序。也就是说,如果两个任务将在同一时间被执行,那么谁先提交谁先执行。
  2. condition,线程等待区,一个线程在获取周期任务执行时,如果还没到执行时间time,那么线程要先到condition中等待,到了time就会唤醒condition中的线程执行任务。

(2)提交的任务被封装为ScheduledFutureTask。
ScheduledFutureTask包含以下几个组件

  1. long类型变量time,表示这个任务将被执行的具体时间
  2. long类型变量 sequenceNumber,这个任务被添加到该线程池的序号
  3. long类型变量 period,,表示这个任务执行的间隔周期

(3)可以在创建该线程池的时候设置核心线程数,但是最大线程数为无限
(4)该线程池创建的线程空闲时间为0
工作原理:当一个任务提交到该线程池时,如果当前线程数小于核心线程数,创建新的工作线程,将该任务放入DelayQueue中,放入过程会通过lock类加锁保证正确放入队列中,过程如下:

  1. 获取lock
  2. 向PriorityBlokingQueue中添加任务
  3. 如果添加的是队列中的头元素,唤醒condition中等待的线程尝试获取任务执行
  4. 释放lock。
    任务添加的同时,线程会到condition中等待,并循环尝试获取队列中的周期任务,获取
    过程通过lock类加锁保证一个任务只会被一个线程正确获取,过程如下:
  5. 获取lock
  6. 尝试从队列中获取周期任务,如果队列为空,线程到condition中等待
  7. 如果不为空,查看队列头元素的执行时间是否还没到,如果是,线程到condition中等待直到可以执行头元素的任务
  8. 如果已经到了执行时间,获取头元素准备执行。获取成功后,如果队列不为空,唤醒condition中等待的线程继续尝试获取任务执行。
  9. 如果一个线程被唤醒后没有获取到任务,那么会无限循环直到获取到队列的头元素。

线程获取到周期任务以后,执行流程如下:

  1. 执行这个task
  2. 修改task的time变量为下次将要被执行的时间
  3. 将这个任务重新添加到PriorityBlokingQueue
  4. 线程到condition中等待,等待队列的头元素可执行后将其唤醒,尝试获取。

猜你喜欢

转载自blog.csdn.net/qq_44727091/article/details/114227538